Spring Framework 에서 Redis 연동 및 사용

redis 는 메모리DB로 NOSql 중 하나.

보통 이중화를 위해 redis를 여러개 띄워 놓고, 이를 sentinel 로 모니터링하는 식으로 사용.

 

sentinel 은 단어가 지니고 있는 뜻(감시자)처럼 redis 를 모니터링 하는 용도로 쓰인다.

보통 홀수개(3, 5...)로 sentinel 을 사용하고,

sentinel들은 redis master 서버가 죽었다고 판단 할 경우, 자체적으로 투표를 진행하여 redis slave를 redis master로 승격시킨다.

 

* Ubuntu에 redis를 설치하여 직접 사용해 보았으나 과정 중 캡쳐를 해놓는다거나 따로 정리하진 못했다.

* redis 자체가 워낙 많이 쓰이고 설치/사용이 어렵지 않아 구글링 조금만 하면 환경은 큰 어려움 없이 잡을 수 있다.

 

문제는 spring framework (jdk1.7)에서 sentinel 및 redis 사용법인데..

우리나라말로 설명이 잘 되어 있는 레퍼런스는 찾기 힘들고, 구글링도 쉽지 않다는 점..

 

현재 프로젝트에서 redis 를 사용하게 될 지 정확한 결론이 나지 않았지만

일단 짬짬히 시간날 때 마다 공부 좀 하고 소스 좀 짜며.. 모듈 구현을 해보았다.

 

* 환경 : 

server : jboss/wildfly 9

language : java

framework : spring 3.x

jdk : 1.7

sentinel 3대, redis 2대

 

***

기존 서비스 내의 채팅서버에서 사용하고 있는 redis서버를 사용했으며, 채팅서버는 vert.x로 구현이 되어 있는 상황.

vert.x 에서 redis 를 사용하기 위해 vert.x 에서 지원하는 lib 을 사용하게되면,

sentinel 에도 비밀번호 설정(sentinel configuration 파일의 require-pass 설정)이 필수적으로 되어 있어야 한다고 하는데, 

문제는 jdk 1.7 에 지원되는 spring redis(jedis) jar엔 sentinel 패스워드가 잡혀있는 환경에서 redis 를 사용할 수 있도록 지원하는 method 가 없는 듯 하다..   

(jedis jar 에 구현되어 있는 method 들(JedisSentinelPool 등..)의 인자값을 보면 password 를 받게 되있는 method 들이 몇개 있었으나, redis password만 인자값으로 받고있을 뿐, sentinel password를 인자값으로 받고 있는 오버로딩 된 생성자가 없다. 혹시나 하고 redis password 인자값 부분에 sentinel password를 넣고 connection을 시도해보았지만 역시나 되지 않음)

 

 

[ pom.xml ]

1
2
3
4
5
6
7
8
9
10
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>1.7.0.RELEASE</version>
        </dependency>
cs

 

lettuce 라는 녀석도 있었지만 jedis 만 sentinel 지원을 한다길래 jedis를 사용했다.

(lettuce 최신 버전은 지원하고 있는지 잘 모르겠다)

 

[ CustomJedisSentinelPool.java ]

sentinel 로부터 master redis를 가져오는 객체.

listener를 멀티스레드로 동작시켜, redis 서버 상태가 변할지라도 redis master를 가져올 수 있게 하는 객체.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
package ~.utils;
 
 
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
 
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolAbstract;
import redis.clients.jedis.JedisPubSub;
import redis.clients.jedis.Protocol;
import redis.clients.jedis.exceptions.JedisConnectionException;
import redis.clients.jedis.exceptions.JedisException;
 
public class CustomJedisSentinelPool extends JedisPoolAbstract {
 
      protected GenericObjectPoolConfig poolConfig;
 
      protected int connectionTimeout = Protocol.DEFAULT_TIMEOUT;
      protected int soTimeout = Protocol.DEFAULT_TIMEOUT;
 
      protected String password;
      protected String sentinelPassword;
      
      protected int database = Protocol.DEFAULT_DATABASE;
 
      protected String clientName;
 
      protected Set<MasterListener> masterListeners = new HashSet<MasterListener>();
 
      protected Logger log = LoggerFactory.getLogger(getClass().getName());
 
      private volatile JedisFactory factory;
      private volatile HostAndPort currentHostMaster;
      
      private final Object initPoolLock = new Object();
      
      public void setSentinelPassword(String sentinelPassword){
          this.sentinelPassword = sentinelPassword;
      }
      
      public CustomJedisSentinelPool(String masterName, Set<String> sentinels,
          final GenericObjectPoolConfig poolConfig) {
        this(masterName, sentinels, poolConfig, Protocol.DEFAULT_TIMEOUT, nullnull,
            Protocol.DEFAULT_DATABASE);
      }
 
      public CustomJedisSentinelPool(String masterName, Set<String> sentinels) {
        this(masterName, sentinels, new GenericObjectPoolConfig(), Protocol.DEFAULT_TIMEOUT, nullnull,
            Protocol.DEFAULT_DATABASE);
      }
 
      public CustomJedisSentinelPool(String masterName, Set<String> sentinels, String password) {
        this(masterName, sentinels, new GenericObjectPoolConfig(), Protocol.DEFAULT_TIMEOUT, password);
      }
 
      public CustomJedisSentinelPool(String masterName, Set<String> sentinels,
          final GenericObjectPoolConfig poolConfig, int timeout, final String password) {
        this(masterName, sentinels, poolConfig, timeout, password, null, Protocol.DEFAULT_DATABASE);
      }
 
      public CustomJedisSentinelPool(String masterName, Set<String> sentinels,
          final GenericObjectPoolConfig poolConfig, final int timeout) {
        this(masterName, sentinels, poolConfig, timeout, nullnull, Protocol.DEFAULT_DATABASE);
      }
 
      public CustomJedisSentinelPool(String masterName, Set<String> sentinels,
          final GenericObjectPoolConfig poolConfig, final String password) {
        this(masterName, sentinels, poolConfig, Protocol.DEFAULT_TIMEOUT, password);
      }
 
      public CustomJedisSentinelPool(String masterName, Set<String> sentinels,
          final GenericObjectPoolConfig poolConfig, int timeout, final String password, final String sentinelPassword ,
          final int database) {
        this(masterName, sentinels, poolConfig, timeout, timeout, password, sentinelPassword, database);
      }
 
      public CustomJedisSentinelPool(String masterName, Set<String> sentinels,
          final GenericObjectPoolConfig poolConfig, int timeout, final String password, final String sentinelPassword,
          final int database, final String clientName) {
        this(masterName, sentinels, poolConfig, timeout, timeout, password, sentinelPassword, database, clientName);
      }
 
      public CustomJedisSentinelPool(String masterName, Set<String> sentinels,
          final GenericObjectPoolConfig poolConfig, final int timeout, final int soTimeout,
          final String password, final String sentinelPassword, final int database) {
        this(masterName, sentinels, poolConfig, timeout, soTimeout, password, sentinelPassword, database, null);
      }
 
      public CustomJedisSentinelPool(String masterName, Set<String> sentinels,
          final GenericObjectPoolConfig poolConfig, final int connectionTimeout, final int soTimeout,
          final String password, final String sentinelPassword, final int database, final String clientName) {
        this.poolConfig = poolConfig;
        this.connectionTimeout = connectionTimeout;
        this.soTimeout = soTimeout;
        this.password = password;
        this.sentinelPassword = sentinelPassword;
        this.database = database;
        this.clientName = clientName;
 
        HostAndPort master = initSentinels(sentinels, masterName);
        initPool(master);
      }
 
      @Override
      public void destroy() {
        for (MasterListener m : masterListeners) {
          m.shutdown();
        }
 
        super.destroy();
      }
 
      public HostAndPort getCurrentHostMaster() {
        return currentHostMaster;
      }
 
      private void initPool(HostAndPort master) {
        synchronized(initPoolLock){
          if (!master.equals(currentHostMaster)) {
            currentHostMaster = master;
            if (factory == null) {
              factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout,
                  soTimeout, password, database, clientName);
              initPool(poolConfig, factory);
            } else {
              factory.setHostAndPort(currentHostMaster);
              // although we clear the pool, we still have to check the
              // returned object
              // in getResource, this call only clears idle instances, not
              // borrowed instances
              internalPool.clear();
            }
 
//            log.info("Created JedisPool to master at " + master);
          }
        }
      }
 
      private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
 
        HostAndPort master = null;
        boolean sentinelAvailable = false;
 
//        log.info("Trying to find master from available Sentinels...");
        
        for (String sentinel : sentinels) {
          final HostAndPort hap = HostAndPort.parseString(sentinel);
 
//          log.debug("Connecting to Sentinel {}", hap);
 
          Jedis jedis = null;
          try {
            jedis = new Jedis(hap);
            
            if(this.sentinelPassword != null && !this.sentinelPassword.isEmpty()){
                jedis.auth(this.sentinelPassword);
            }
            
            List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
 
            // connected to sentinel...
            sentinelAvailable = true;
 
            if (masterAddr == null || masterAddr.size() != 2) {
//              log.warn("Can not get master addr, master name: {}. Sentinel: {}", masterName, hap);
              continue;
            }
 
            master = toHostAndPort(masterAddr);
//            log.debug("Found Redis master at {}", master);
            break;
          } catch (JedisException e) {
            // resolves #1036, it should handle JedisException there's another chance
            // of raising JedisDataException
            log.warn(
              "Cannot get master address from sentinel running @ {}. Reason: {}. Trying next one.", hap,
              e.toString());
          } finally {
            if (jedis != null) {
              jedis.close();
            }
          }
        }
 
        if (master == null) {
          if (sentinelAvailable) {
            // can connect to sentinel, but master name seems to not
            // monitored
            throw new JedisException("Can connect to sentinel, but " + masterName
                + " seems to be not monitored...");
          } else {
            throw new JedisConnectionException("All sentinels down, cannot determine where is "
                + masterName + " master is running...");
          }
        }
 
//        log.info("Redis master running at " + master + ", starting Sentinel listeners...");
 
        for (String sentinel : sentinels) {
          final HostAndPort hap = HostAndPort.parseString(sentinel);
          MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
          // whether MasterListener threads are alive or not, process can be stopped
          masterListener.setDaemon(true);
          masterListeners.add(masterListener);
          masterListener.start();
        }
 
        return master;
      }
 
      private HostAndPort toHostAndPort(List<String> getMasterAddrByNameResult) {
        String host = getMasterAddrByNameResult.get(0);
        int port = Integer.parseInt(getMasterAddrByNameResult.get(1));
 
        return new HostAndPort(host, port);
      }
 
      @Override
      public Jedis getResource() {
        while (true) {
            try {
                Jedis jedis = super.getResource();
                jedis.setDataSource(this);
                
                // get a reference because it can change concurrently
                final HostAndPort master = currentHostMaster;
                final HostAndPort connection = new HostAndPort(jedis.getClient().getHost(), jedis.getClient()
                        .getPort());
                
                if (master.equals(connection)) {
                    // connected to the correct master
                    return jedis;
                } else {
                    returnBrokenResource(jedis);
                }
            } catch (Exception e) {
                e.printStackTrace();
                throw e;
            }
        }
      }
 
      @Override
      protected void returnBrokenResource(final Jedis resource) {
        if (resource != null) {
          returnBrokenResourceObject(resource);
        }
      }
 
      @Override
      protected void returnResource(final Jedis resource) {
        if (resource != null) {
          resource.resetState();
          returnResourceObject(resource);
        }
      }
 
      protected class MasterListener extends Thread {
 
        protected String masterName;
        protected String host;
        protected int port;
        protected long subscribeRetryWaitTimeMillis = 5000;
        protected volatile Jedis j;
        protected AtomicBoolean running = new AtomicBoolean(false);
 
        protected MasterListener() {
        }
 
        public MasterListener(String masterName, String host, int port) {
          super(String.format("MasterListener-%s-[%s:%d]", masterName, host, port));
          this.masterName = masterName;
          this.host = host;
          this.port = port;
        }
 
        public MasterListener(String masterName, String host, int port,
            long subscribeRetryWaitTimeMillis) {
          this(masterName, host, port);
          this.subscribeRetryWaitTimeMillis = subscribeRetryWaitTimeMillis;
        }
 
        @Override
        public void run() {
 
          running.set(true);
 
          while (running.get()) {
 
            j = new Jedis(host, port);
            if(sentinelPassword != null && !sentinelPassword.isEmpty()){
                j.auth(sentinelPassword);
            }
            try {
              // double check that it is not being shutdown
              if (!running.get()) {
                break;
              }
              
              /*
               * Added code for active refresh
               */
              List<String> masterAddr = j.sentinelGetMasterAddrByName(masterName);  
              if (masterAddr == null || masterAddr.size() != 2) {
                log.warn("Can not get master addr, master name: {}. Sentinel: {}:{}.",masterName,host,port);
              }else{
                  initPool(toHostAndPort(masterAddr)); 
              }
 
              j.subscribe(new JedisPubSub() {
                @Override
                public void onMessage(String channel, String message) {
                  log.debug("Sentinel {}:{} published: {}.", host, port, message);
 
                  String[] switchMasterMsg = message.split(" ");
 
                  if (switchMasterMsg.length > 3) {
 
                    if (masterName.equals(switchMasterMsg[0])) {
                      initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
                    } else {
                      log.debug(
                        "Ignoring message on +switch-master for master name {}, our master name is {}",
                        switchMasterMsg[0], masterName);
                    }
 
                  } else {
                    log.error(
                      "Invalid message received on Sentinel {}:{} on channel +switch-master: {}", host,
                      port, message);
                  }
                }
              }, "+switch-master");
 
            } catch (JedisException e) {
 
              if (running.get()) {
                log.error("Lost connection to Sentinel at {}:{}. Sleeping 5000ms and retrying.", host,
                  port, e);
                try {
                  Thread.sleep(subscribeRetryWaitTimeMillis);
                } catch (InterruptedException e1) {
                  log.error("Sleep interrupted: ", e1);
                }
              } else {
                log.debug("Unsubscribing from Sentinel at {}:{}", host, port);
              }
            } finally {
              j.close();
            }
          }
        }
 
        public void shutdown() {
          try {
            log.debug("Shutting down listener on {}:{}", host, port);
            running.set(false);
            // This isn't good, the Jedis object is not thread safe
            if (j != null) {
              j.disconnect();
            }
          } catch (Exception e) {
            log.error("Caught exception while shutting down: ", e);
          }
        }
      }
    }
cs

*

jedis jar의 JedisSentinelPool.class 를 열어보면 아래 소스와 거의 일치한다..

그럼에도 불구하고 기존 구현된 jedis jar의 JedisSentinelPool 을 사용하지 않고 굳이 JedisPoolAbstract을 상속받아 구현하게 된 이유는, Jedis jar의 JedisSentinelPool은 Sentinel에 패스워드 설정이 잡혀있는 경우, initSentinel(..) method내에서 sentinel 로 부터 redis master 정보를 가져오는 jedis.sentinelGetMasterAddrByName(..) method 에서 Exception 이 나기 때문.. (auth fail exception)

 

실질적으로 수정한 부분은 jedis 객체로부터 master 정보를 가져오는 sentinelGetMasterAddrByName(..)메소드를 호출하기 전에 jedis객체에 sentinel 패스워드를 set해준게 전부..

(163~165 line, 298~301 line)

 

 

[ JedisFactory.java ]

CustomJedisSentinelPool.java 에서 사용하는 객체로, 

Jedis jar 에 구현되어 있지만 접근제한자가 default이므로 동일한 package가 아닌 CustomJedisSentinelPool에선 해당 객체를 생성할 수 없어 CustomJedisSentinelPool과 동일한 package 밑에 JedisFactory를 만들어 줌.

* Jedis jar 내의 JedisFactory.class 와 동일한 소스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package ~.utils;
 
import java.net.URI;
import java.util.concurrent.atomic.AtomicReference;
 
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocketFactory;
 
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.DefaultPooledObject;
 
import redis.clients.jedis.BinaryJedis;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.InvalidURIException;
import redis.clients.jedis.exceptions.JedisException;
import redis.clients.jedis.util.JedisURIHelper;
 
/**
 * PoolableObjectFactory custom impl.
 */
class JedisFactory implements PooledObjectFactory<Jedis> {
  private final AtomicReference<HostAndPort> hostAndPort = new AtomicReference<HostAndPort>();
  private final int connectionTimeout;
  private final int soTimeout;
  private final String password;
  private final int database;
  private final String clientName;
  private final boolean ssl;
  private final SSLSocketFactory sslSocketFactory;
  private final SSLParameters sslParameters;
  private final HostnameVerifier hostnameVerifier;
 
  JedisFactory(final String host, final int port, final int connectionTimeout,
      final int soTimeout, final String password, final int database, final String clientName) {
    this(host, port, connectionTimeout, soTimeout, password, database, clientName,
        falsenullnullnull);
  }
 
  JedisFactory(final String host, final int port, final int connectionTimeout,
      final int soTimeout, final String password, final int database, final String clientName,
      final boolean ssl, final SSLSocketFactory sslSocketFactory, final SSLParameters sslParameters,
      final HostnameVerifier hostnameVerifier) {
    this.hostAndPort.set(new HostAndPort(host, port));
    this.connectionTimeout = connectionTimeout;
    this.soTimeout = soTimeout;
    this.password = password;
    this.database = database;
    this.clientName = clientName;
    this.ssl = ssl;
    this.sslSocketFactory = sslSocketFactory;
    this.sslParameters = sslParameters;
    this.hostnameVerifier = hostnameVerifier;
  }
 
  JedisFactory(final URI uri, final int connectionTimeout, final int soTimeout,
      final String clientName) {
    this(uri, connectionTimeout, soTimeout, clientName, nullnullnull);
  }
 
  JedisFactory(final URI uri, final int connectionTimeout, final int soTimeout,
      final String clientName, final SSLSocketFactory sslSocketFactory,
      final SSLParameters sslParameters, final HostnameVerifier hostnameVerifier) {
    if (!JedisURIHelper.isValid(uri)) {
      throw new InvalidURIException(String.format(
        "Cannot open Redis connection due invalid URI. %s", uri.toString()));
    }
 
    this.hostAndPort.set(new HostAndPort(uri.getHost(), uri.getPort()));
    this.connectionTimeout = connectionTimeout;
    this.soTimeout = soTimeout;
    this.password = JedisURIHelper.getPassword(uri);
    this.database = JedisURIHelper.getDBIndex(uri);
    this.clientName = clientName;
    this.ssl = JedisURIHelper.isRedisSSLScheme(uri);
    this.sslSocketFactory = sslSocketFactory;
    this.sslParameters = sslParameters;
    this.hostnameVerifier = hostnameVerifier;
  }
 
  public void setHostAndPort(final HostAndPort hostAndPort) {
    this.hostAndPort.set(hostAndPort);
  }
 
  @Override
  public void activateObject(PooledObject<Jedis> pooledJedis) throws Exception {
    final BinaryJedis jedis = pooledJedis.getObject();
    if (jedis.getDB() != database) {
      jedis.select(database);
    }
 
  }
 
  @Override
  public void destroyObject(PooledObject<Jedis> pooledJedis) throws Exception {
    final BinaryJedis jedis = pooledJedis.getObject();
    if (jedis.isConnected()) {
      try {
        try {
          jedis.quit();
        } catch (Exception e) {
        }
        jedis.disconnect();
      } catch (Exception e) {
 
      }
    }
 
  }
 
  @Override
  public PooledObject<Jedis> makeObject() throws Exception {
    final HostAndPort hostAndPort = this.hostAndPort.get();
    final Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort(), connectionTimeout,
        soTimeout, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
 
    try {
      jedis.connect();
      if (password != null) {
        jedis.auth(password);
      }
      if (database != 0) {
        jedis.select(database);
      }
      if (clientName != null) {
        jedis.clientSetname(clientName);
      }
    } catch (JedisException je) {
      jedis.close();
      throw je;
    }
 
    return new DefaultPooledObject<Jedis>(jedis);
 
  }
 
  @Override
  public void passivateObject(PooledObject<Jedis> pooledJedis) throws Exception {
    // TODO maybe should select db 0? Not sure right now.
  }
 
  @Override
  public boolean validateObject(PooledObject<Jedis> pooledJedis) {
    final BinaryJedis jedis = pooledJedis.getObject();
    try {
      HostAndPort hostAndPort = this.hostAndPort.get();
 
      String connectionHost = jedis.getClient().getHost();
      int connectionPort = jedis.getClient().getPort();
 
      return hostAndPort.getHost().equals(connectionHost)
          && hostAndPort.getPort() == connectionPort && jedis.isConnected()
          && jedis.ping().equals("PONG");
    } catch (final Exception e) {
      return false;
    }
  }
}
 
cs


[ RedisUtil.java ]

CustomJedisSentinelPool 을 사용하여 redis master 객체를 얻어오고, redis에 값을 넣고 빼는 등 실질적으로 redis를 사용하는 클래스

 

별도의 property로 sentinel ip:port, sentinel password, redis password, database number를 관리.

* sentinel 로 부터 redis 객체를 가져오기 때문에 redis ip:port 는 별도로 관리해줄 필요가 없음.

 

getConnection() : pool 객체 생성(내부적으로 listener 생성 및 동작)

getValue(String key) : key 기준으로 값을 가져오는 메소드

setValue(String key, String value, int sec) : key, value 를 특정시간(sec)동안 redis 에 저장하는 메소드

disConnection(..) : 사용한 redis 와의 접속 해제 및 자원 반납

                        (* 자원 반납 제대로 하지 않을 경우 server exhausted Exception 발생)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package ~.utils;
 
import java.util.HashSet;
import java.util.Set;
 
import javax.annotation.PostConstruct;
 
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
 
import redis.clients.jedis.Jedis;
 
import ~.APIException;
 
@Component("RedisUtil")
public class RedisUtil {
    
    @Value("#{properties['sentinel.Addr']}")
    private String SENTINEL_ADDR;
    
    @Value("#{properties['sentinel.master']}")
    private String MASTER_NAME;
    
    @Value("#{properties['sentinel.pa']}")
    private String SENTINEL_P;
    
    @Value("#{properties['sentinel.redis.pa']}")
    private String REDIS_P;
    
    @Value("#{properties['sentinel.db']}")
    private int DATABASE;
 
    private Set<String> sentinels;
    
    private final String deli = "\\^";
    
    private CustomJedisSentinelPool pool;
    
    protected static final Logger logger = LoggerFactory.getLogger(RedisUtil.class);
    
    @PostConstruct
    private void getConnection(){
        
        if(pool != null){
            return;
        }
        
        
        if(sentinels == null){ 
        
            sentinels = new HashSet<String>();
        }
        
        if(sentinels.isEmpty()){
        
            String[] sentinelArr = this.SENTINEL_ADDR.split(this.deli);
            
            for(String sentinelNode : sentinelArr ){
        
                sentinels.add(sentinelNode);
            }
        }
        
        logger.debug("redis master name : " + this.MASTER_NAME);
        logger.debug("redis redis p : " + this.REDIS_P);
        logger.debug("redis sentinel p : " + this.SENTINEL_P);
        logger.debug("redis database : " + this.DATABASE);
        
        GenericObjectPoolConfig gopc = new GenericObjectPoolConfig();
        gopc.setMaxWaitMillis(1000);
        gopc.setBlockWhenExhausted(false);
        pool = new CustomJedisSentinelPool(this.MASTER_NAME, 
                sentinels, gopc, 500this.REDIS_P, this.SENTINEL_P, this.DATABASE);
        
        logger.info("get Connection 3");
    }
    
    public String getValue(String key){
        String result="";
 
        Jedis jedis = null;
        
        
        try{
            jedis = this.pool.getResource();
        
        } catch (Exception e){
            logger.error("redis connection fail : " + e);
        }
        
        result = jedis.get(key);
        logger.debug("retrived value : " + result);
        
        
        this.disconnection(jedis);
        
        return result;
    }
    
    public boolean setValue(String key, String value, int sec){
        boolean result=true;
        Jedis jedis = null;
        
 
        try{
            jedis = this.pool.getResource();
            String setexResult = jedis.setex(key, sec, value);
        
        
            if(!"OK".equalsIgnoreCase(setexResult)){
                throw new APIException();
            }
            
        } catch (APIException ae){
            logger.warn("set redis data fail : " + ae);
            result = false;
        } catch (Exception e){
            logger.error("exception in set value : " + e);
            result = false;
        }
        logger.info("set value 3");
        this.disconnection(jedis);
        
        return result;
    }
        
    private void disconnection(Jedis jedis){
        try{
 
                if(jedis != null){
                    jedis.disconnect();
                    jedis.close();
                    jedis = null;
                }
 
        } catch (Exception e){
            logger.error("redis disconnection fail : {}" , e);
        }
    }
 
    
}
 
cs

 

RedisTemplate 객체를 사용할 경우 값을 다양한 방법으로 넣고 뺄 수 있던데,

해당 부분은 나중에 공부를 더 해보는걸로..

 

반응형

자바 컴파일 버전 Exception

 

컴파일된 앱(ex: lib/jar)이 실행하는 환경의 jdk 버전보다 높은 경우 아래와 같은 exception이 발생. 

Exception in thread "main" java.lang.UnsupportedClassVersionError: ~ : Unsupported major.minor version 52.0

여기서 version 52.0 은 java 8버전을 의미(1.8)

 

 

참고 :

J2SE 8   = Version 52

J2SE 7   = Version 51

J2SE 6.0 = Version 50

J2SE 5.0 = Version 49

JDK  1.4 = Version 48

JDK  1.3 = Version 47

JDK  1.2 = Version 46

JDK  1.1 = Version 45



해결 방법 :

jar/lib 을 본인(서버)의 실행환경(jdk버전)에 맞게 버전을 낮춰주거나,

본인(서버)의 실행환경을 사용할 jar/lib에 맞춰주어야 한다. 

보통 전자의 방법으로 해결..

반응형

스프링프레임워크 log4j2 설정 : Spring Framework + log4j 2 + slf4j + jboss/wildfly(ver 9)

 

*  jdk : 1.7

*  framework : spring

*  was : jboss/wildfly 9

 

#기본개념

slf4j 는 interface, 

log4j 는 구현체

 

slf4j 사용시 소스에서 interface , imple 사용하듯 구현체만 바꾸면 돼서 

log4j 를 나중에 걷어내고 다른 log lib을 사용해도 소스레벨에서 수정할게 없음. 그래서 slf4j 사용.

 

log4j 2는 log4j 1랑 다른거 없이 log4j 2.x 대 버전을 의미.

(log4j 1.x 는 log4j 라고부르고 log4j 2 는 log4j 2.x 를 말하는 듯)

* log4j2 사용시 slf4j는 1.7.x 이상 사용

 

참고 : https://goddaehee.tistory.com/45

(위 고수님의 블로그에 log4j, slf4j, logback 에 관한 내용이 상세히 기술되어 있음)

 

[ # log4j 2 및 slf4j maven dependency 설정 ]

log4j-slf4j-impl jar(actifactId) 사용시 1.7.x 이하 version 사용

log4j-slf4j18-impl jar(actifactId) 사용시 1.8.x 이상 version 사용

참고 : https://logging.apache.org/log4j/2.x/log4j-slf4j-impl/index.html

 

[ pom.xml ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<modelVersion>4.0.0</modelVersion>
    <groupId>com.sample</groupId>
    <name>SAMPLE</name>
    <packaging>war</packaging>
    <version>0.1</version>
    <properties>
        <!-- 생략 -->
        <org.slf4j-version>1.7.5</org.slf4j-version>
        <org.log4j-version>2.9.0</org.log4j-version>
    </properties>
 
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>${org.springframework-version}</version>
    <exclusions>
        <!-- Exclude Commons Logging in favor of slf4j -->
        <exclusion>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
         </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>${org.slf4j-version}</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>${org.log4j-version}</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>${org.log4j-version}</version>
    <exclusions>
         <!-- Exclude Commons Logging in favor of log4j-core -->
         <exclusion>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
         </exclusion>
    </exclusions>
</dependency>
cs

* spring 의 경우 default 로 jcl(commons-logging)사용하므로 이를 slf4j 로 대체하기 위해 jcl-over-slf4j 사용

* jcl(commons-logging) 대신 slf4j 를 사용하므로 spring-context 에서 commons-logging 제거(exclude)

* jcl-over-slf4j 내부적으로 slf4j-api dependency 를 포함하고 있으므로 slf4j-api 는 별도의 dependency 로 추가 하지 않음

* log4j-core 내에 log4j-api dependency 포함하므로 log4j-slf4j-impl 에서 해당 의존성 제거

 

[ #log4j2.xml 설정 ]

src/main/resources/ 경로에 생성(/WEB-INF/classes/).

해당 위치에 파일을 위치시킬 경우 log4j 가 초기화 될 때 알아서 해당 파일을 읽어들임

 

* src/main/resource/ 밑이 아닌 기타 경로에 해당 파일을 위치시킬 경우

web.xml 에 아래와 같이 추가

참고 : https://okky.kr/article/282263

 

[ web.xml ]

1
2
3
4
5
6
7
8
9
  <context-param>
    <param-name>log4jConfigLocation</param-name>
    <param-value>classpath:~~~/log4j2.xml</param-value>
  </context-param>
  
  <listener>
    <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
  </listener>
 
cs

 

[ log4j2.xml ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>    
    <Appenders>
        <Console name="console_root" target="SYSTEM_OUT">
            <PatternLayout pattern="%d %5p %m%n" />
        </Console>
         <Console name="console_com" target="SYSTEM_OUT">
            <PatternLayout pattern="%d %5p [%c] %m%n" />
        </Console>       
    </Appenders>
    
    
    <Loggers>
         <Logger name="java.sql" level="INFO" additivity="false">
            <AppenderRef ref="console_com" />
        </Logger>
        <Logger name="jdbc.sqltiming" level="INFO" additivity="false">
            <AppenderRef ref="console_com" />
        </Logger>
        <Logger name="egovframework" level="INFO" additivity="false">
            <AppenderRef ref="console_com" />
        </Logger>
        <Logger name="com" level="INFO" additivity="false">
            <AppenderRef ref="console_com" />
        </Logger>
        <Logger name="org.springframework" level="INFO" additivity="false">
            <AppenderRef ref="console_com" />
        </Logger>
        <Root level="INFO">
            <AppenderRef ref="console_root" />
        </Root>
    </Loggers>
</Configuration>
 
cs

 

[* 어펜더 (Appender) ]

어펜더는 로그이벤트를 목적지에 전달하는 역할을 한다.(타겟 목적지에 이벤트 데이터를 쓰는 역할)

어펜더는 Appender interface 를 구현한다.

name 속성을 가지며 이를 통해 로거에서 참조한다.

아래와 같은 어펜더가 존재.

ConsoleAppender : Sysout 을 찍는 용도로 사용되는 어펜더

FileAppender : 로그를 파일로 저장할 용도로 사용되는 어펜더

RollingFileAppender : FileAppender 에 Rollover 기능(파일 백업)을 추가한 어펜더

 

[* 로거 (Logger) ]

로거의 위계구조(hierarchy)에 대한 이해가 필요하다. 여기를 읽어보길 바란다.

아래는 로거 위계구조의 예이다.

com.sample.sampleController 클래스 내에서
protected Logger logger = LoggerFactory.getLogger(this.getClass());
logger.info("샘플로깅!");
과 같이 로그를 출력할 경우,
this.getClass()com.sample.sampleController를 반환하고,
해당 클래스는 com 패키지 하위의 클래스(com.*) 이므로 "com"이란 이름(name)으로 명명된 logger설정을 따른다.

* 만약 23~25line 의 "com" logger 를 제거한다면 com 패키지 하위 소스내(com.sample.sampleController)에서 찍은 로그는 Root 로거의 console_root 어펜더에 의해 출력된다. ("com" logger 있을 경우 : console_com(%d %5p [%c] %m%n) appender 에 의해 출력, 없을 경우 root logger인 console_root(%d %5p %m%n)  에 의해 출력

* 이와 같은 위계구조를 활용하여 라이브러리 내부에서 불필요 혹은 원치않는 로그를 출력하고 있는 경우, 해당 라이브러리 패키지 경로를 별도의 logger 로 잡아 로그 출력을 제거 할 수 있다.

* Root 로거는 매핑되는 logger 를 찾지 못하는 경우 가장 마지막에 사용되는 최상위 로거이며, Root 로거는 반드시 설정해주어야 한다.

 

[* 패턴 (PatternLayout) ]

%d 날짜 년월일시

%c : 클래스명

%5p : 로그레벨(5자리 길이 맞춰서 출력(5자리 이하일경우 공백열로 채워넣음))

%m : 메시지

%n : 줄바꿈

%t : 스레드명

 

[ # jboss 에서 log4j 사용시 필요한 설정 ]

: jboss 내부적으로 slf4j, log4j 라이브러리를 내장하고 있기 때문에

해당 부분을 exclude 하여 충돌요소를 제거

 

[ jboss-deployment-structure.xml ]

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<jboss-deployment-structure>
    <deployment>
        <exclusions>
            <module name="org.apache.log4j" />
            <module name="org.apache.commons.logging" />
            <module name="org.slf4j"/>
        </exclusions>
    </deployment>
</jboss-deployment-structure>
cs

참고 : http://blog.naver.com/PostView.nhn?blogId=djawl_2&logNo=220665045226&redirect=Dlog&widgetTypeCall=true
(위 고수님의 블로그에 jboss 환경변수를 이용하여 충돌을 막는 방법도 기재되어 있음)

 

[ # jboss 에서 log4j 2.10 버전 이상 사용시 발생하는 문제 ]

jboss 에서 log4j 2 2.10 이상 버전 사용시 발생하는 warn 로그 :

WARN  [org.jboss.as.server.deployment] (MSC service thread 1-5) WFLYSRV0003: Could not index class META-INF/versions/9/module-info.class at /content/example.war/WEB-INF/lib/log4j-api-2.11.1.jar: java.lang.IllegalStateException: Unknown tag! pos=4 poolCount = 32

서버동작에 문제는 없으나 was 기동시 WARN 로그 찍히는게 보기가 싫으니,  log4j 버전을 살짝 내려서 사용.

참고 : https://issues.jboss.org/browse/JBEAP-15261

(module-info.class 가 log4j 2.10 이상 버전에 존재하므로 해당 경고가 발생)

 

참고 : log4j 2 user guide

반응형

emoji 처리

모바일 앱이 전달하는 채팅내용에 emoji 가 포함되어 API 서버에서 Mysql DB에 해당 채팅내용을 로그성으로 기록하기 위해 insert를 하다 DB에러가 났다.

Exception은 아래와 같이 Incorrect string value ~ 발생.

Warning: #1366 Incorrect string value: '\xF0\x9F\x8D\xB0' for column 'Text' at row ~

 

* emoji 는 모바일(android, ios)에서 사용되는 이모티콘 같이 생긴 문자열로

3byte 문자열인 utf8과 달리 4byte 문자열인 utf8mb4 charset 을 사용한다.

 

 

해결방법 1. 

DB 칼럼의 datatype을 utf8mb4로 수정을 하여 해결

 

해결방법 2.

2-1) emoji 를 정규식으로 거른다 :

android, ios os 버전업이 될 때마다 emoji가 추가되는 듯 하니, 오래 운영될 서비스라면 적합하지 않은 방법인 듯.

2-2) 문자를 제외한 모든 데이터를 정규식으로 거른다 :

허용해줄 모든 문자들을 신중히 남겨놓아야 하기 때문에 번거롭겠지만, 로그성 데이터라면 대충 걸러도 되니 해당 방법을 선택..

1
2
3
4
5
6
public static String replaceEmoji(String text){
 
    String newText = text.replaceAll("[^a-zA-Z0-9ㄱ-힣\\s!@#$%^&|:<>~/';\"`.\\?\\}\\{\\|\\*\\[\\]\\(\\)-_/]""▩");
        
   return newText;
}
cs

 

 

 

반응형

운영, 테스트서버, 개발서버 와 같이 서버가 3개가 구성되어있고,

각각의 서버별로 사용하는 jar가 달라야 할 때, 서버 환경에 맞게 jar를 빌드하여 서버에 배포해보자.

 

* 서버 환경별로 jar 내부적으로 소켓통신을 하는 목적지 ip가 달라 jar가 총 3개 존재했고..

   war 를 어떤 서버에 배포하느냐에 따라 jar를 바꿔서 빌드 후, 서버에 배포해야 했던 상황

** 소스내에서 갖다 쓰는 jar 내부의 메소드 자체를 수정하여 목적지 서버 ip를 파라미터로 받게끔 처리해주는게 베스트..

    하지만 jar 만드신 분이 본인보다 상급자라면 별 수 없다.

 

 

1) pom.xml 에 profiles 설정 및 local repository 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<repositories>
      <repository>
         <id>local-repository</id>
         <url>file:${basedir}/repo</url>
      </repository>
       </repositories>
    
    <profiles>
      <profile>
         <id>dev</id>
         <dependencies>
            <dependency>
               <groupId>chat</groupId>
               <artifactId>chatmodule-dev</artifactId>
               <version>0.0.6</version>
            </dependency>
         </dependencies>
      </profile>
      <profile>
         <id>tb</id>
         <dependencies>
            <dependency>
               <groupId>chat</groupId>
               <artifactId>chatmodule-tb</artifactId>
               <version>0.0.6</version>
            </dependency>
         </dependencies>
      </profile>
      <profile>
         <id>live</id>
         <dependencies>
            <dependency>
               <groupId>chat</groupId>
               <artifactId>chatmodule-</artifactId>
               <version>0.0.6</version>
            </dependency>
         </dependencies>
      </profile>
   </profiles>
cs

 

2) 프로젝트 내에 local repository 구성

repo > chat > chatmodule/chatmodule-dev/chatmodule-tb > 0.0.6 > chatmodule-~.jar 와 같이 구성

chatmodule-~.jar : 운영서버 배포시 사용될 jar

chatmodule-tb~.jar : 테스트서버 배포시 사용될 jar

chatmodule-dev~.jar : 개발서버 배포시 사용될 jar

 

 

3) Maven Profile 선택

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Maven > Select Maven Profiles 클릭.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

활성화 시킬 profile(dev/tb/live) 선택

 

 

4) Maven update

 

5) Maven Build (war 파일 생성)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

profile(dev/tb/live) 선택

 

 

6) war 파일 및 jar 확인

빌드가 성공적으로 끝났으면,

workspace/프로젝트/war파일명/target/WEB-INF/lib/

경로로 이동하여 지정한 profile 의 jar가 잘 들어갔는지 확인.

 

 

위와 같이 profile 을 사용하여 환경별 jar 를 동적으로 사용하는게 아닌,

단순히 로컬(프로젝트 내)에 존재하는 jar 1개를 사용하고자 할 경우

local repository 를 잡을 필요 없이 아래와 같이 scope 를 system 으로 주고 프로젝트 내에서 jar 를 가져다 사용할 수 있다.

1
2
3
4
5
6
7
<dependency>
    <groupId>chat</groupId>
    <artifactId>mobile</artifactId>
    <version>0.0.1</version>
    <scope>system</scope>
    <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/chat_0.0.1.jar</systemPath>
</dependency>
cs

 

반응형

기존에 interceptor 로 로그를 출력하도록 소스를 짜놓았지만 리소스를 너무 잡아먹는 듯 하여

AOP에서 로그를 출력하도록 바꾸었다.

 

설명은 나중에 채워넣겠다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package ;
 
import java.util.HashMap;
import java.util.Map;
 
import javax.servlet.http.HttpServletRequest;
 
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.ModelAndView;
 
 
@Component
@Aspect
public class ControllerAOP {
 
    protected static final Logger logger = LoggerFactory.getLogger(ControllerAOP.class);
 
    @Around("execution(public org.springframework.web.servlet.ModelAndView com.sample.bmp.mobile..*Controller.*(..))")
    public ModelAndView around(ProceedingJoinPoint pjp) {
        ModelAndView mav = null;
 
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
 
        Object inputParam = null;
        for (Object obj : pjp.getArgs()) {
            if (obj instanceof Map) {
                inputParam = obj;
            }
        }
 
        long start = System.currentTimeMillis();
 
        String controller = (String) pjp.getTarget().getClass().getSimpleName();
 
        String path = request.getRequestURI();
        String addr = request.getRemoteAddr();
        int port = request.getRemotePort();
 
        logger.info("##########################################################################");
        logger.info("# REQUEST | CONTROLLER = {} | METHOD = {} | REMOTEADDR = {} | PORT = {} | IN_PARAMS = {}",
                controller, path, addr, port, inputParam == null ? "": inputParam);
        logger.info("##########################################################################");
 
        Map<String, Object> result = null;
        
        try {
            mav = (ModelAndView) pjp.proceed();
            result = mav.getModel();
        } catch (APIException e) {
            mav = new ModelAndView("jsonView");
            result = new HashMap<String, Object>();
//            e.printStackTrace();
            result.put("result_cd", e.getErrCode());
            result.put("result_msg", e.getMessage());
 
            mav.addAllObjects(result);
        } catch (Throwable e) {
            mav = new ModelAndView("jsonView");
            result = new HashMap<String, Object>();
//            e.printStackTrace();
            result.put("result_cd"0);
            result.put("result_msg""");
 
            mav.addAllObjects(result);
        } finally {
            String success = "fail";
            
            if (result.containsKey("result_cd")) {
                try {
                    if ("1".equals(result.get("result_cd").toString())) {
                        success = "success";
                    }
                } catch (ClassCastException | NumberFormatException e) {
                    logger.debug("SessionInterceptor error == {}", e);
                }
            }
            
 
            long end = System.currentTimeMillis();
            logger.info("##########################################################################");
            logger.info("# RESPONSE | CONTROLLER = {} | METHOD = {} | RESULT = {} | REMOTEADDR = {} | PORT = {} | TIME = {} ms | IN_PARAMS = {} | OUT_PARAMS = {}",
                    controller, path, success, addr, port, end - start,
                    inputParam == null ? "" : inputParam, 
                    result == null ? "" : result.toString());
            logger.info("##########################################################################");
 
        }
 
        return mav;
    }
 
}
cs
반응형

보통 서버 로그를 분석하는 툴(awstats 등)을 서버내에 설치하거나

로그를 다운받은 후 로그 분석 툴을 돌려 로그를 분석하는 걸로 알고있다.

 

하지만 분석을 excel 로 할 테니, 서버 로그를 row 데이터로 뽑아 달라는 갑의 요구사항이 있었다..

(운영은 자기네들이 하면서 왜 이걸 나한테 하라고.....)

 

 

1. 로그에서 필요한 부분들만 발췌하여 row 데이터로 만들어 주는 shell script 를 짠다.

[로그 형식]

2019-04-15 18:50:37,511 INFO  [stdout] (default task-40) INFO : com.test.sample.interceptor.ControllerAOP - # RESPONSE | CONTROLLER = ApiController | METHOD = /mobile/api/getvoddata | RESULT = success | REMOTEADDR = 127.0.0.1 | PORT = 8082 | TIME = 9 ms | IN_PARAMS = {key1=value1, key2=value2, key3=value3} | OUT_PARAMS = {key1=value1, key2=value2}

서버 로그가 위와 같이 쌓이고 있다.

서버 로그에서 불필요한 부분들(검은색 폰트)은 제거하고 남길 데이터들(빨강색 폰트)만 row데이터로 남겨, txt 파일을 생성하는 shell script를 만들어보자.

 

makelowdata.sh 파일 생성

> touch makelowdata.sh 

makelowdata.sh 파일 vi 모드로 편집

> vi makelowdata.sh

 

쉘스크립트 실행시

첫번째 매개변수를 분석대상로그파일,

두번째 매개변수를 추출할 row 데이터의 시작기준시각,

세번째 매개변수를 추출할 row 데이터의 종료기준시각

으로 가정하고 작성

 

[작성 완료한 makelowdata.sh 쉘스크립트]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#!/bin/sh
LOG_FILE=$1
FROM_TIME=$2
END_TIME=$3
 
if [ $LOG_FILE ] ;
        then
        echo "LOG_FILE is null"
fi
 
if [ $FROM_TIME ] ;
        then
        echo "FROM_TIME is null"
fi
 
if [ $END_TIME ] ;
        then
        echo "END_TIME is null"
fi
 
 
echo "---------------------------------------"
echo "analysing logging starts... "
echo "target log file : $1"
 
if [ -f logrowdata_$2_$3.txt ] ;
       then
           echo "logrowdata_$2_$3 exists"
           exit
        else
           touch logrowdata_$2_$3.txt
fi
 
 
 
var1=`awk -v frd="$FROM_TIME" -v et="$END_TIME" 'frd <= $2 && $2 <= et' $LOG_FILE | grep -'success|fail' | sed 's/IN_PARAMS = //' | sed 's/OUT_PARAMS = //' | awk '{print $1 "\t" $2 "\t" $9 "\t" $16 "\t" $20 "\t" $24 "\t" $28 "\t" $32 "\t" $36}'`
var2=`awk -v frd="$FROM_TIME" -v et="$END_TIME" 'frd <= $2 && $2 <= et' $LOG_FILE | grep -'success|fail' | sed 's/IN_PARAMS = //' | sed 's/OUT_PARAMS = //' | cut -d'|' -f8-9 | sed 's/|/\t/'`
 
 
if [ -f var1.txt ] ;
     then
         rm ./var1.txt
fi
 
if [ -f var2.txt ] ;
     then
         rm ./var2.txt
fi
 
 
echo "$var1" > var1.txt
echo "$var2" > var2.txt
 
 
paste -"\t" var1.txt var2.txt > logrowdata_$2_$3.txt
 
 
 
 
 
 
rm var1.txt var2.txt
 
 
echo "analysing logging has been finished... "
echo "---------------------------------------"
 
cs

 

 

[해석]

6~9 line : 매개변수 1 유효성검사

11~14 line : 매개변수 2 유효성검사 (시간 형식 체크는 굳이 하지 않음..)

16~19 line : 매개변수 2 유효성검사 (시간 형식 체크는 굳이 하지 않음..)

26~32 line : 결과물인 logrowdata_시작시각_종료시각.txt 파일 존재여부 확인 후, 존재할 경우 해당 파일 삭제, 존재하지 않을 경우 파일 생성

-f filename : 파일 존재할 시 true 리턴

36 line : 로그 파일(매개변수 1)에서 필요부분 추출 후 var1 변수에 선언

[한 줄 씩 해석]

1) var1=

>> var1 변수 선언 및 저장

2) `awk -v frd="$FROM_TIME" -v et="$END_TIME" 'frd <= $2 && $2 <= et' $LOG_FILE | 

awk -v 변수="값" -v 변수="값" '정규식' 파일

>> 전역변수 $LOG_FILE 의 파일 중 2번째 필드($2) 가 frd 보다 크거나 같고 et 보다 작거나 같은 row만 뽑는다. 

>> 전역변수 $FROM_TIME, $END_TIME을 각각 frd, et 변수에 저장, awk 정규식 내에서 변수로 사용 (awk -v 옵션 사용)

3) grep -E 'success|fail' | 

grep -E '문자열 조건 문자열'

>>  2)번으로 부터 전달받은 문자열 중, success 혹은 fail 문자열이 포함된 row만 뽑는다. (grep -E 옵션 사용)

4) sed 's/IN_PARAMS = //' | 

sed 's/치환대상문자열/대체문자열/'

>> 3)번으로 부터 전달받은 문자열에서 "IN_PARAMS = " 값을 ""로 치환한다. (sed 's~' 사용)

sed 's/OUT_PARAMS = //' | 

>> 위와 동일

5) awk '{print $1 "\t" $2 "\t" $9 "\t" $16 "\t" $20 "\t" $24 "\t" $28 "\t" $32 "\t" $36}'`

>> 4) 단계 까지 처리한 row 에서 1,2,9,16,20,24,28,32,36 번째 필드를 중간중간 탭("\t") 구분자를 두어 출력 

 

37 line : 로그 파일(매개변수 1)에서 필요부분 추출 후 var2 변수에 선언

[한 줄 씩 해석]

1) var2=

>> var2 변수 선언 및 저장

2) `awk -v frd="$FROM_TIME" -v et="$END_TIME" 'frd <= $2 && $2 <= et' $LOG_FILE | 

awk -v 변수="값" -v 변수="값" '정규식' 파일

>> 전역변수 $LOG_FILE 의 파일 중 2번째 필드($2) 가 frd 보다 크거나 같고 et 보다 작거나 같은 row만 뽑는다. 

>> 전역변수 $FROM_TIME, $END_TIME을 각각 frd, et 변수에 저장, awk 정규식 내에서 변수로 사용 (awk -v 옵션 사용)

3) grep -E 'success|fail' | 

grep -E '문자열 조건 문자열'

>>  2)번으로 부터 전달받은 문자열 중, success 혹은 fail 문자열이 포함된 row만 뽑는다. (grep -E 옵션 사용)

4) sed 's/IN_PARAMS = //' | 

sed 's/치환대상문자열/대체문자열/'

>> 3)번으로 부터 전달받은 문자열에서 "IN_PARAMS = " 값을 ""로 치환한다. (sed 's~' 사용)

sed 's/OUT_PARAMS = //' | 

>> 위와 동일

5) cut -d'|' -f8-9 | 

cut -d'구분문자' -f추출할인덱스

>> 4) 단계 까지 처리한 row 에서 "|" 문자 기준으로 문자열을 분류하여(java split 함수와 비슷) 8~9 번째 문자열만 뽑는다. (cut -d -f 옵션 사용)

6) sed 's/|/\t/'` 

>> 5) 단계 까지 처리한 row 에서 "|" 문자열을 탭("\t")으로 치환

 

40~43 line : var1.txt 파일 존재시 삭제

45~48 line : var2.txt 파일 존재시 삭제

51 line : var1 변수를 var1.txt 파일로 생성

52 line : var2 변수를 var2.txt 파일로 생성

55 line : 탭("\t")을 구분으로 하여 var1.txt, var2.txt 파일을 합친 후(각각의 로우 기준으로 append), logrowdata_매개변수2_매개변수3.txt 파일로 저장

>> paste -d "\t" 파일1 파일2 > 결과파일 : -d 옵션을 사용하여 특정 문자를 구분자로 사용하여 두 문자열을 합친 후 결과파일로 생성

62 line : 임시 파일로 생성했던 var1.txt, var2.txt 를 삭제

 

 

2. 1번 쉘스크립트를 돌려 row 파일 생성

위에서 작성한 쉘스크립트 실행

> ./makelowdata.sh server.log 18:00 19:00

( server.log 로그파일에서 18:00 ~ 19:00시 사이의 로그만 추출 ) 

[쉘스크립트로 추출한 로그]

2019-04-15 18:30:37,511  com.test.sample.interceptor.ControllerAOP  ApiController  

/mobile/api/getvoddata  success  127.0.0.1  8082  9  {key1=value1, key2=value2, key3=value3}  {key1=value1, key2=value2}

 

3. 2번에서 생성한 row 파일을 엑셀 파일로 변환

1) 쉘스크립트로 생성한 txt 파일을 엑셀 파일로 연다.

2) "구분 기호로 분리됨" 선택, char encoding "949 : 한국어" 선택

3) 구분 기호 "탭" 선택

4) 열 데이터 서식 "텍스트" 선택

4. 필터등을 적용하여 분석한다.

분석은 알아서.

 

 

awk 로 로그 뽑을 줄 알았다면 로그 찍을 때 스페이스바 난사를 하지 않았을텐데.. 다음엔 이러지 말아야지.

책 사서 리눅스 공부도 해야겠다..

 

반응형

로컬에서 awstats 설치 및 사용

 

awstats 을 사용하여 access.log 를 분석해보자

 

1. perl 설치

2. awstats 설치

3. 사용

 

순서로 진행.

 

 

1. perl 설치

딸기가 가장 많이 쓰이는것 같으니 스트로베리로 설치.

http://strawberryperl.com/

접속

빨간 테두리로 둘러쌓인 Recommended version에서 본인pc 환경에 맞는걸로 다운로드

 

다운로드 끝나면 실행 및 설치..(생략)

 

 

2. awstats 설치

1) http://www.awstats.org/#DOWNLOAD  접속 후, 

빨간 테두리로 둘러쌓인 링크 중, awstats~.zip 클릭하여 다운로드

 

2) 압축 해제

 

3) wwwroot/cgi-bin/awstats.model.conf 파일을 복사 후

awstats.프로젝트명.conf 와 같이 카피.

(awstats.model.conf 는 예제 파일)

 

4) 해당 파일을 문서편집기로 열어 아래 부분들을 수정

a) LogFile="E:/awstats/logs/access.log"   (분석 대상인 로그파일의 경로)

b) SiteDomain="localhost" (로컬에서 돌려볼거니 로컬호스트로 설정)

c) HostAliases="localhost 127.0.0.1" 

d) DirIcons="../icon" (레포트로 나오는 html상의 이미지 파일 경로를 상대경로로 지정)

e) LogFormat="분석대상인 apache서버 로그(access.log)의 포맷을 확인하여 작성"   (분석 대상인 로그파일의 포맷 지정)

apache 서버접속 후 conf 디렉토리 및의 httpd.conf 에서 access.log 포맷을 확인

(빨간 테두리로 둘러쌓인 부분 확인)

 

아파치 로그 포맷의 문법?과 awstats 에서 사용되는 문법이 약간씩 다르기 때문에,

https://www.internetofficer.com/awstats/log-format/

위 사이트를 참고하여 apache access.log의 약어들이 각각 어떤 awstats 약어와 일치하는지 확인 후,

LogFormat 을 작성한다.

 

* 매칭되지 않는 access.log 의 %D (duration microseconds) 같은 경우 extra1 와 같이 선언 후 기타 설정을 해줘야 한다는데..

   나중에 필요하면 알아보는걸로.

 

3. 사용(실행)

1) 실행(awstats)

cmd console 창을 띄워

wwwroot/cgi-bin/ 경로로 이동후

>awstats.pl -update -config=프로젝트명(awstats.프로젝트명.conf 에서 프로젝트명에 해당)

명령어 입력

 

2) 레포트 생성

awstats.pl  -staticlinks -config=sample -output > result/awstats.프로젝트명.html
* 이외에도 여러가지 종류의 레포트를 뽑아낼 수 있는듯 하나 자세한건 나중에 알아보는걸로.

 

 

awstats.프로젝트명.html 을 열어본다.

 

awstats.프로젝트명.html 

 

 

참고 : https://reiphiel.tistory.com/entry/awstats-local-execution

거의 모든 부분을 위의 블로그에서 참고했다.(감사합니다)

 

 

분석결과로 뽑아낸 레포트를 보고 로그를 분석하기가 쉽지 않을 듯하여 그냥 쉘스크립트를 작성하는걸로.....(awstats은 나중에 다시 써보는걸로..)

 

 

 

반응형

[기본 명령어]

1. OS 확인

grep . /etc/*-release

 

2. ip 확인

ifconfig

 

3. 계정생성

useradd 계정명

 

4. 계정패스워드변경

passwd 계정명

 

5. 파일 및 폴더에 권한 부여

chmod 755 파일명

 

유저(user)   : read 읽기(4) write 쓰기(2) execute 실행(1) = 4 + 2 + 1 = 7

그룹(group) : read 읽기(4) write 쓰기(2) execute 실행(1) = 4      + 1 = 5

기타(other)  : read 읽기(4) write 쓰기(2) execute 실행(1) = 4      + 1 = 5

Ex]
> chmod 755 start.sh

start.sh 파일에 대해
유저는 읽기(4) 쓰기(2) 실행(1),
그룹은 읽기(4) 실행(1),
기타는 읽기(4) 실행(1) 권한을 부여

 

6. 파일 및 폴더의 소유자 변경

chown 소유계정:소유그룹 파일명

 

ex) chown jboss:jboss start.sh

start.sh 파일에 대한 소유권을 jboss 계정, jboss 그룹으로 바꾼다.

 

7. 파일 읽기

cat 파일명;

 

8. 파일 찾기

find -name '*log*'

: 현재 디렉토리 하위에서 log 문자열이 포함된 파일명 기준으로 찾기

 

* grep

: 보통 grep 만 사용하는 경우는 드물고 파이프(|)와 같이 사용함.

Ex]
grep '기준이되는문자열'
grep -v '제외시킬 문자열'
grep -e '기준문자열1||기준문자열2'
grep -A 3 -B 1 '기준이되는문자열' : -A(after) 기준문자열 뒤로 3줄 -B(before) 이전 1줄까지 출력
cat '파일' | grep '파일내에 찾을 문자열'

9. 삭제

파일 제거

rm 파일명

디렉토리제거

rmdir 디렉토리명

파일 및 디렉토리 전부삭제

rm -rf 디렉토리명

 

프로그램설치

yum : CentOS

apt : Ubuntu

 

10. 압축

.zip 파일 현재위치에 풀기

> unzip file.zip -d .

 

11. 복사 관련

1) directory 통째로 복사

cp -r ./directory .

(omitting ~ 관련 에러가 날 경우 -r 옵션주기)

 

 

<mysql 관련>

1. mysql 접속

mysql -u mysql아이디 -p

 

2. 데이터베이스(스키마) 보기

show databases;

 

3. use 데이터베이스명(스키마);

 

4. 테이블목록 보기

show tables;

 

 

[vi (linux visual editor) 관련]

1. vi 실행

vi 파일명

 

* 좌측 line number 보기

:set number

 

2. 문서 끝으로 이동

G (입력모드아닌 상태에서)

:$

 

2-1. 문서 첫 부분으로 이동

gg (입력모드아닌 상태에서)

 

3. 특정 문자를 찾아 해당 문자 위치로 이동

3-1. 순차검색

:/ 찾을문자열

3-2. 역순검색 

:? 찾을문자열 

 

3-1. 문자 찾기 커서 이동(이전/다음 문자로 이동)

방향대로 이동: n

역방향으로 이동: N

Ex] server.log 파일에서 exception 이 들어간 문자열을 vi 모드에서 찾고싶은 경우

1. 문서의 끝에서 부터 찾기(backward 방향으로 서칭하기)
> vi server.log
> :$
>:?exception
n (현재 탐색 방향(backward)으로 이동하며 exception 문자열이 포함된 부분으로 커서 이동 
N (현재 탐색 방향과 반대방향(forward)으로 이동하며 exception 문자열이 포함된 부분으로 커서 이동

2. 문서의 시작점부터 찾기(forward 방향으로 서칭하기)
>vi server.log
(입력모드 아닌 상태에서) gg 
>:/exception
n (현재 탐색 방향(forward)으로 이동하며 exception 문자열이 포함된 부분으로 커서 이동
N (현재 탐색 방향과 반대방향(backward)으로 이동하며 exception 문자열이 포함된 부분으로 커서 이동

 

4. 문자 지우기

delete 키

한줄 지우기 : 입력모드아닌 상태에서 dd

 

5. 문자열 치환 

%s/대상문자열/치환할문자열/g (전역치환)

%s/대상문자열/치환할문자열/i (대소문자구분없이 치환)

 

6. 문자 입력 (INSERT 모드로 전환)

i 입력

INSERT 모드 종료는 ctrl + c 혹은 esc

 

7. 종료

6-1. 저장 후 종료

:wq

6-2. 저장

:w

6-3. 저장 없이 종료

:q!

 

[crontab 관련]

crontab [-u user] file
        crontab [-u user] [ -e | -l | -r ]
                (default operation is replace, per 1003.2)
        -e      (edit user's crontab)
        -l      (list user's crontab)
        -r      (delete user's crontab)
        -i      (prompt before deleting user's crontab)
        -s      (selinux context)

1. crontab -l

목록보기

 

2. crontab -e

edit 모드

 

3. crontab -r 

전체 제거

*사용 자체를 하지말 것

 

 

[네트워크 관련]

1. 네트워크 상태 보기 

netstat

 

2. 방화벽 확인

2-1. telnet ip port

현재 서버에서 ip port 에 대한 방화벽 확인

telnet 종료키 : ctrl + ]

 

2-2. ping domain or ip

현재 서버에서 domain 에 대한 ping 확인, ip 알아내기

ex]

> ping naver.com

> Ping naver.com [210.89.164.90] 32바이트 데이터 사용:

> telnet 210.89.164.90 80

> ctrl + ]

 

2-3. netstat -tnlp

열려있는 포트 확인

 

<프로세스 관련>

1. 프로세스 확인

ps -ef

 

2. 프로세스 중 특정 프로세스만 확인

ps -ef | grep 프로그램명

파이프(|) 사용

 

3. 프로세스 죽이기

kill -9 프로세스번호

*프로세스 번호는 ps -ef | grep 프로그램명 으로 확인

 

4. 서버 상태 확인(CPU, RAM 등)

top

 

<방화벽 관련>

firewalld : CentOS

ufw : Ubuntu

서버 ftp 설정 (vsftpd): 참고

 

[bash 관련]

which/whereis/locate 참고

1. which

명령어의 위치 찾기

which find

which -a find

 

2. whereis

명령어의 실행파일위치, 소스위치, man 파일위치

whereis find

 

3. locate

패턴 사용하여 파일 위치 찾기

locate *.sh

locate -n 10 *.sh 패턴에 속한 파일 중에 지정한 개수만큼만 검색

 

 

반응형

 MYSQL GROUP BY(incompatible with sql_mode=only full group by)에 대해 알아보자

 

로컬 및 개발 환경에서 멀쩡히 돌아가던 쿼리가

테스트 서버에 올리고 나니 다음과 같은 Exception을 뱉었다.

com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column '칼럼명' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

: group by 절에 포함되지 않은 칼럼(집계되지 않은 칼럼(nonaggregated column))을 select 절에서 뽑아올 경우, 어느 칼럼을 표시해야 할 지 몰라 exception 이 발생하는 현상이다. 

only_full_group_by 란?

mysql 5.7버전부터 sql_mode로 추가된 설정으로

집계되지 않은 칼럼을 select 절에서 뽑아오는 경우 exception을 낼지 exception 을 내지 않고 동작할지 결정하는 옵션이다.

 

간단히 예를 들면.

아래와 같이 사람 별로 영어 점수 총 합계를 조회하는 쿼리가 있다.

1
2
3
4
5
SELECT id as '아이디'
       ,  phone_num as '전화번호'
       ,  SUM(eng_point) as '영어점수합계'
FROM  sampletable
GROUP BY id
cs

phone_num (전화번호) 은 유니크한 값으로 다른 레코드와 중복될 수 없는 값이란 걸 개발자는 알겠지만,

DB입장에선 phone_num의 경우 어떤 값을 표시해야 할지 알 수 없다. (이를 집계되지 않은 칼럼 nonaggregated column)이라 칭함)

 

* 동일한 쿼리를 ORACLE 에서 실행시 적합하지 않은 group by 사용으로 exception 이 발생한다.

* mysql 에서 실행시 only_full_group_by 옵션의 활성화/비활성화 상태에 따라 exception이 발생/발생하지 않는다.

 

[해결 방법]

1. 쿼리를 수정한다.

1) 쿼리 자체를 수정. 

1-1) group by 절에 nonaggregated column 추가

1
2
3
4
5
6
SELECT  id as '아이디'
       ,  phone_num as '전화번호'
       ,  SUM(eng_point) as '영어점수합계'
FROM  sampletable
GROUP BY id, phone_num
 
cs

 

1-2) 서브쿼리 사용(경우에 따라 left outer join 사용)

1
2
3
4
5
6
7
  SELECT  id as '아이디'
       ,  phone_num as '전화번호'
       ,  ( SELECT SUM(eng_point) 
            FROM  sampletable
            WHERE st.id = id 
            GROUP BY id) as '영어점수합계'            
FROM  sampletable st
cs

 

2) select 절의 집계되지 않은 칼럼에 ANY_VALUE(칼럼) 함수 추가

1
2
3
4
5
SELECT  id as '아이디'
       ,  ANY_VALUE(phone_num) as '전화번호'
       ,  SUM(eng_point) as '영어점수합계'
FROM  sampletable
GROUP BY id
cs

 

2. mysql 설정파일을 수정한다. 

2-1) 현재 옵션 상태를 확인한다.

 

sql_mode 조회.

> select @@sql_mode;

 

sql_mode 조회 결과.

| ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,                                                                                        DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION |

ONLY_FULL_GROUP_BY 이 포함되어 있다면 해당 설정이 활성화 되어 있음을 의미.

 

2-2) my.cnf 설정파일을 열어 해당 부분을 찾아 제거

2-3) mysql 재기동.

 

※ 머릿속에서 나온 예제이므로 적합하지 않을 수 있으며, 실제 동작하지 않는 쿼리일 수도 있다.

 애초에 쿼리를 잘 짜면 이 같은 문제가 나올 수 없다...

 

반응형

+ Recent posts