MongoDB ORM框架Morphia缓存小坑

最近在用Morphia更新MongoDB数据时发现更新完后没有拿到更新后的新数据,一开始没有在意以为MongDB复制集读写分离后拿的从服务器数据,从服务器还没有同步到更新。后来一想不太对,复制集的op同步的间隔很短,更新和查询又是在一个上下文中,而且当时读写相当空闲,虽然NOSQL都是保证最终一致性,但如果这么简单的一个操作都没办法保证原子性那也太搓了,而且用MongoDB这么多年也没发现类似的问题。

重现代码:

Test
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test() {
Query<Blog> q = datastore.createQuery(Blog.class).field("author").equal("b");
System.out.println(q.get());

Query<Blog> ctQ = datastore.createQuery(Blog.class);
ctQ.field("author").equal("b");
UpdateOperations<Blog> u = datastore.createUpdateOperations(Blog.class);
u.set("post", "zzzz");
datastore.update(ctQ, u);

System.out.println(q.get());
}

执行一下查询,然后在更新,在查询。根据日志显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#####第一次执行查询,post是1111111
Creating MappedClass for class mongo.service.Blog
[TRACE] 2017-11-02 09:21:00,506 [main] [org.mongodb.morphia.query.QueryImpl] - Running query(Blog) : { "author" : "b"}, fields:null,off:0,limit:1
[TRACE] 2017-11-02 09:21:00,506 [main] [org.mongodb.morphia.query.QueryImpl] - Getting cursor(Blog) for query:{ "author" : "b"}
[DEBUG] 2017-11-02 09:21:00,506 [main] [org.mongodb.driver.protocol.command] - Sending command {find : BsonString{value='Blog'}} to database PTMO_TEST on connection [connectionId{localValue:2, serverValue:7577}] to server 26.47.136.186:57004
[DEBUG] 2017-11-02 09:21:00,508 [main] [org.mongodb.driver.protocol.command] - Command execution completed
Blog(_id=59fa6b4b934f0741cd03663e, author=b, post=1111111, comments=null)


####### 执行更新,更新post为zzzz
[TRACE] 2017-11-02 09:21:00,513 [main] [org.mongodb.morphia.DatastoreImpl] - Executing update(Blog) for query: { "author" : "b"}, ops: { "$set" : { "post" : "zzzz"}}, multi: true, upsert: false
[DEBUG] 2017-11-02 09:21:00,520 [main] [org.mongodb.driver.protocol.update] - Updating documents in namespace PTMO_TEST.Blog on connection [connectionId{localValue:2, serverValue:7577}] to server 26.47.136.186:57004
[DEBUG] 2017-11-02 09:21:00,529 [main] [org.mongodb.driver.protocol.update] - Update completed



######第二次执行查询,查询出来的对象post仍然是1111111
[TRACE] 2017-11-02 09:21:00,531 [main] [org.mongodb.morphia.query.QueryImpl] - Running query(Blog) : { "author" : "b"}, fields:null,off:0,limit:1
[TRACE] 2017-11-02 09:21:00,531 [main] [org.mongodb.morphia.query.QueryImpl] - Getting cursor(Blog) for query:{ "author" : "b"}
[DEBUG] 2017-11-02 09:21:00,532 [main] [org.mongodb.driver.protocol.command] - Sending command {find : BsonString{value='Blog'}} to database PTMO_TEST on connection [connectionId{localValue:2, serverValue:7577}] to server 26.47.136.186:57004
Blog(_id=59fa6b4b934f0741cd03663e, author=b, post=1111111, comments=null)

就是这迷惑性的日志,让我觉得每次 q.get()就是向数据库进行查询操作。
我一开始以为是数据没有落实所以读到是老数据,于是修改update时 WriteConcern进行更新。

Test
1
datastore.update(ctQ,u, false, WriteConcern.JOURNALED);
  • JOURNALED: 确保数据到磁盘上的事务日志中才返回。
  • MAJORITY: 大多数数据节点确认后才返回。
  • ACKNOWLEDGED: 默认机制,服务器收到就返回。
  • UNACKNOWLEDGED: socket write后没报异常就返回。
  • W1/W2/W3: 1/2/3个成员确认后才返回。

然而仍然没什么用,不是数据落实问题。后来想到MongoDB中原子性的更新是 findAndModify ,于是改成findAndModify:

Test
1
2
3
4

// datastore.update(ctQ, u);
Blog b = datastore.findAndModify(ctQ, u);
System.out.println(b);

执行后确实拿到了最新的数据,虽然问题可以换成FAM解决但仍然想知道问题的原因,于是跟了一下get()的源码。

Query get的实现类 QueryImpl:

QueryImpl
1
2
3
4
5
6
7
8
@Override
public T get() {
final int oldLimit = limit;
limit = 1;
final Iterator<T> it = fetch().iterator();
limit = oldLimit;
return (it.hasNext()) ? it.next() : null;
}

实际上是fetch方法执行的查询

QueryImpl
1
2
3
4
5
6
7
8
9
@Override
public MorphiaIterator<T, T> fetch() {
final DBCursor cursor = prepareCursor();
if (LOG.isTraceEnabled()) {
LOG.trace("Getting cursor(" + dbColl.getName() + ") for query:" + cursor.getQuery());
}

return new MorphiaIterator<T, T>(ds, cursor, ds.getMapper(), clazz, dbColl.getName(), cache);
}

在prepareCursor()方法中,确实可以看到框架封装了DBObject以及DBCollection,并确确实实的调用了find方法进行查询:

QueryImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Prepares cursor for iteration
*
* @return the cursor
*/
public DBCursor prepareCursor() {
final DBObject query = getQueryObject();
final DBObject fields = getFieldsObject();

if (LOG.isTraceEnabled()) {
LOG.trace("Running query(" + dbColl.getName() + ") : " + query + ", fields:" + fields + ",off:" + offset + ",limit:" + limit);
}

final DBCursor cursor = dbColl.find(query, fields);
cursor.setDecoderFactory(ds.getDecoderFact());
...
}

但是返回的cursor封装到了MorphiaIterator里,并且传参跟随着一个cache。
MorphiaIterator也是Iterator的实现,其中封装next操作,在获取到DBOBject进行反序列化操作,具体反序列化操作调用的Mapper类中的方法:

Mapper
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
/**
* Converts a DBObject back to a type-safe java object (POJO)
*
* @param <T> the type of the entity
* @param datastore the Datastore to use when fetching this reference
* @param dbObject the DBObject containing the document from mongodb
* @param entity the instance to populate
* @param cache the EntityCache to use
* @return the entity
*/
public <T> T fromDb(final Datastore datastore, final DBObject dbObject, final T entity, final EntityCache cache) {
//hack to bypass things and just read the value.
if (entity instanceof MappedField) {
readMappedField(datastore, (MappedField) entity, entity, cache, dbObject);
return entity;
}

// check the history key (a key is the namespace + id)

if (dbObject.containsField(ID_KEY) && getMappedClass(entity).getIdField() != null
&& getMappedClass(entity).getEntityAnnotation() != null) {
final Key<T> key = new Key(entity.getClass(), getCollectionName(entity.getClass()), dbObject.get(ID_KEY));
final T cachedInstance = cache.getEntity(key);
if (cachedInstance != null) {
return cachedInstance;
} else {
cache.putEntity(key, entity); // to avoid stackOverflow in recursive refs
}
}

final MappedClass mc = getMappedClass(entity);

final DBObject updated = mc.callLifecycleMethods(PreLoad.class, entity, dbObject, this);
try {
for (final MappedField mf : mc.getPersistenceFields()) {
readMappedField(datastore, mf, entity, cache, updated);
}
} catch (final MappingException e) {
Object id = dbObject.get(ID_KEY);
String entityName = entity.getClass().getName();
throw new MappingException(format("Could not map %s with ID: %s in database '%s'", entityName, id,
datastore.getDB().getName()), e);
}

if (updated.containsField(ID_KEY) && getMappedClass(entity).getIdField() != null) {
final Key key = new Key(entity.getClass(), getCollectionName(entity.getClass()), updated.get(ID_KEY));
cache.putEntity(key, entity);
}
mc.callLifecycleMethods(PostLoad.class, entity, updated, this);
return entity;
}

可以看到22行-28行是有缓存操作的,缓存的key是集合名和_id值,也就是说虽然第二个get确实执行了查询操作,但由于_id一样所以Morphia直接获取了cachedInstance操作,导致拿到的是第一次的缓存结果。
根据cache的注释来看,在同一个query对象的查询返回的都是缓存。
所以后续的查询其实有一部分是为了获取_id值用于拼cache key的,如果命中就取缓存。
比较坑的是如果有多次查询,并且查询结果比较大,后续的查询其实是浪费,这种缓存机制应该交给开发者进行控制,还能省一些查询。

Tomcat在部署应用时卡住

最新在一台新的CentOS上部署环境,当部署完Tomcat8准备启起来看看效果时发现一直阻塞在部署应用这条日志下。

1
Deploying web application directory /tomcat/webapps/live

记得新下载的tomcat解压后直接启动一点问题没有,但为了安全清空了webapps下的tomcat ROOT和Manage应用后,并把我的应用传上来在启动就卡住了,一直无解。
后来google到了一篇文章

1
2
3
4
5
6
7
8
9
One thing I noticed on one of my first upgrades is that TC7 can often
take a long time to start up due to slow initialization of the
SessionIdGenerator - it can take up to nearly 2 minutes! It appears
to take longer if I restart TC7 quickly which seems to confirm that a
lack of entropy is the issue.

org.apache.catalina.util.SessionIdGenerator-: Creation of SecureRandom
instance for session ID generation using [SHA1PRNG] took [105,014]
milliseconds.

大体意思为Tomcat7+依赖 SecureRandom 提供的随机数给分布式Session做ID,取决于JRE,可能会导致Tomcat启动的延迟,上面这个人大概等了2分钟左右。

有两种解决方法.
1.修改Tomcat catalina.sh配置,设置非阻塞的SecureRandom初始化。

1
-Djava.security.egd=file:/dev/./urandom

2.修改JVM配置$JAVA_PATH/jre/lib/security/java.security。

1
securerandom.source=file:/dev/urandom

替换成

1
securerandom.source=file:/dev/./urandom

修改后,启动时间恢复正常。

利用NFS进行Linux之间的目录共享

最近需要在linux下共享个目录到另外一台linux下用用,如果有桌面环境用鼠标点点就可以轻松配个samba出来,但服务器版则要改一堆配置才行,太麻烦。后来突然想到如果只是linux之间的共享不涉及到windows其实用NFS就可以,还简单一点。

NFS简单描述:

NFS是一个sun创建的网络协议,NFS运行在一个区域网直接进行文件共享。etc…

1.修改/etc/exports文件。

exports文件用于暴露需要共享的文件给NFS客户端,是一个访问控制列表。

2.添加要共享的目录到文件。

1
2
vi /etc/exports
/home/xxx/ttt/ 26.47.136.*(rw,sync,no_root_squash)

配置以空格分割。

  • /home/xxx/ttt/ 指要共享的目录。
  • 26.47.136.* 术语叫:Machine Name Formats,理解为客户端的授权方式,这里配置的允许客户端来自26.47这个网段,还支持域名、nis netgroups等方式。
  • 括号里的是配置项
    • rw 允许读写
    • sync 确保文件存储到磁盘后才响应客户端,与之对应的是async。
    • no_root_squash 使用者如果是root,那么对于这个目录他就有root权限。一般正式环境不建议用这个选项,不安全,与之对应的是root_squash。

保存修改后,NFS服务端的配置已经完成。运行脚本启动NFS服务:

1
2
/etc/init.d/portmap start
/etc/init.d/nfs start

3.另外一台服务器做为客户端挂载共享目录。

1
2
mount -t nfs 26.47.136.19:/home/xxx/ttt /mnt/xxx
ls /mnt/xxx

挂载类型使用nfs,就可以将共享目录挂载到/mnt/xxx下。

出现的问题

1
failed: RPC Error: Program not registered

使用命令查看NFS服务端的服务器进程:

1
ps aux|grep rpc

正常应该有两个进程 rpc.statd 和 rpc.mountd,如果没有启动则手动启起来,一般在/usr/sbin下。

1
Access deny

检查下iptables,hosts.allow等配置。

在基于Spring-Boot的Jersey中集成Swagger

最近一时兴起,想将最近几年工作中用到的微服务栈,提干,优化一下作为Seed项目记录一下,省着忘了。
my-microservice-seed

本来想引入Swagger来作为Api描述与设计,以前在一个基于Spring REST开发的一个小后台项目中简单的初探过,配置起来很方便。
只要引入io.springfox的两个依赖,在提供一个配置类,配置类中在指定下下RestControlle所有在的包既可通过swagger-ui.html看到生成的描述。like this

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
/**
* @author liuxinsi
* @mail akalxs@gmail.com
*/
@Configuration
@EnableSwagger2
public class Swagger {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.lxs.mms.rs.resource"))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("MMS")
.description("my microservice seed")
.version("1.0.0")
.contact(new Contact("liuxinsi", "https://liuxinsi.github.io", "akalxs@gmail.com"))
.build();
}
}

但这次试用的是Jersey,配置后没办法产生文档,根据日志看感觉配置的resource下都没扫到东西。最后在io.springfox与Swagger的github wiki上查了一下.

原来springfox提供的Swagger实现仅能作用于基于Spring MVC的REST实现,Jersey不行。。
想要用Swagger对Jersey生成的接口产生描述文档则需要:

  • 用Swagger-api提供的Swagger-core对Jersey的resource产生Swagger2的描述文件(类似WADL/WSDL)即swagger.json。
  • 在github上下载swagger-ui.
  • 用直接运行html或docker或nginx等运行swagger-ui。
  • 调整跨域策略或让swagger-ui与Jersey在同一域下。
  • 访问生成的swagger.json
  • all shit done.

乍一看颇为麻烦,但其实除了要手动处理下swagger-ui以外其他也还好,都是Swagger需要的步骤无非是自己做还是springfox帮你做好了。

完整代码

1
2
3
4
5
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-jersey2-jaxrs</artifactId>
<version>1.5.0</version>
</dependency>

引入swagger依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Swagger配置。
*
* @author liuxinsi
* @mail akalxs@gmail.com
*/
@Component
@Log4j2
public class Swagger {
@PostConstruct
public void initSwagger() {
log.debug("init Swagger ");
BeanConfig config = new BeanConfig();
config.setTitle("MMS");
config.setDescription("my microservice seed");
config.setVersion("1.0.0");
config.setContact("liuxinsi");
config.setSchemes(new String[]{"http", "https"});
config.setBasePath(JerseyConfig.APPLICATION_PATH);
config.setResourcePackage(JerseyConfig.RESOURCE_PACKAGE_NAME);
config.setPrettyPrint(true);
config.setScan(true);
}
}

配置整个服务的相关信息以及Resource所在包

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
package com.lxs.mms.rs.resource;

import io.swagger.jaxrs.listing.ApiListingResource;
import io.swagger.jaxrs.listing.SwaggerSerializers;
import org.glassfish.jersey.logging.LoggingFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.slf4j.bridge.SLF4JBridgeHandler;
import org.springframework.stereotype.Component;

import javax.ws.rs.ApplicationPath;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* jersey 相关配置。
*
* @author liuxinsi
* @mail akalxs@gmail.com
*/
@Component
@ApplicationPath(JerseyConfig.APPLICATION_PATH)
public class JerseyConfig extends ResourceConfig {
public static final String APPLICATION_PATH = "services";
public static final String RESOURCE_PACKAGE_NAME = "com.lxs.mms.rs.resource";
/**
* 覆盖jersey logging 自带的jul logger
*/
private static final Logger REQ_RESP_LOGGER = Logger.getLogger("payload-logger");

public JerseyConfig() {
// 移除根日志处理器
SLF4JBridgeHandler.removeHandlersForRootLogger();
// 绑定新的处理器
SLF4JBridgeHandler.install();
// 请求 响应日志
REQ_RESP_LOGGER.setLevel(Level.FINE);
LoggingFeature lf = new LoggingFeature(REQ_RESP_LOGGER);
register(lf);

// 配置Swagger
this.register(ApiListingResource.class);
this.register(SwaggerSerializers.class);

packages(RESOURCE_PACKAGE_NAME);
}
}

注册两个feature,一个用于生成api信息,一个用于编解码产生swagger.json。

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
package com.lxs.mms.rs.resource.user;

import com.lxs.mms.rs.resource.ResourceSupport;
import com.lxs.mms.rs.resource.bean.user.UserInfo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.stereotype.Component;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
* @author liuxinsi
* @mail akalxs@gmail.com
*/
@Component
@Path("/user/v1")
@Api(value = "用户相关服务", produces = "application/json")
public class UserResource extends ResourceSupport {
@ApiOperation(value = "加载所有用户", notes = "要分页")
@GET
@Path("/loadUsers")
public List<UserInfo> loadUsers() {
List<UserInfo> userInfos = new ArrayList<>(10);
for (int i = 0; i < 100; i++) {
UserInfo u = new UserInfo();
u.setId(i + "");
u.setName("u" + i);
u.setPwd("");
u.setRegisteDate(new Date());
userInfos.add(u);
}
return userInfos;
}
}

Resource类,注意@Api注解。

这时可以先启动spring-boot,访问http://127.0.0.1:8888/services/swagger.json,如生成了swagger2的信息则配置都成功。
部署swagger-ui,我是将下载到的静态资源直接部署在spring-boot的resource/static中,让swagger-ui的请求与swagger在一个域下。
最后重新启动spring-boot访问swagger-ui界面。


all shit done。

Linux下FTP服务搭建完成后的一些设置

今天在centos下搭建里一个ftp 服务,但是使用commen-net包下的ftpclient进行连接登录时报错

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
private boolean connect() {
log.debug("ftp status:{}/{}", ftpClient.isAvailable(), ftpClient.isConnected());
if (!ftpClient.isConnected()) {
log.debug("ftp client 失去连接");
try {
ftpClient.logout();
} catch (Exception e) {
log.debug("注销异常", e);
}
try {
ftpClient.disconnect();
} catch (Exception e) {
log.debug("关闭连接异常", e);
}
try {
ftpClient.connect(BDConfig.getString("fs.ftp.ip"), BDConfig.getInt("fs.ftp.port"));
} catch (IOException e) {
log.error("连接到:{}:{}异常", BDConfig.getString("fs.ftp.ip"), BDConfig.getString("fs.ftp.port"), e);
return false;
}

boolean connect = FTPReply.isPositiveCompletion(ftpClient.getReplyCode());
log.debug("ftp 连接完毕:{}。", connect, ftpClient.getReplyString());

if (!Strings.isEmpty(Config.getString("fs.ftp.userName")) && !Strings.isEmpty(Config.getString("fs.ftp.password"))) {
try {
boolean b = ftpClient.login(Config.getString("fs.ftp.userName"), Config.getString("fs.ftp.password"));
log.debug("{}/{}登录结果:{}/{}", Config.getString("fs.ftp.userName"), Config.getString("fs.ftp.password"), b, ftpClient.getReplyString());
} catch (IOException e) {
log.error("登录到:{}:{}异常", Config.getString("fs.ftp.ip"), Config.getString("fs.ftp.port"), e);
return false;
}
}

try {
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
} catch (IOException e) {
log.error("设置文件类型异常,{}", e, ftpClient.getReplyString());
return false;
}
}
return true;
}

连接没问题,登录报错

1
ftpuser2/ftpuser2登录结果:false/500 OOPS: cannot change directory:/home/ftpuser2

一开始以为是用户的问题,重新配置了一下vsftpd仍然不行。
最后google下发现linux搭建好ftp server还要配置几个开关项才能使用。使用命令查看ftp的配置项

1
sestatus -b|grep ftp

1
2
3
4
5
6
7
8
9
10
11
12
allow_ftpd_anon_write                       off
allow_ftpd_full_access off
allow_ftpd_use_cifs off
allow_ftpd_use_nfs off
ftp_home_dir off
ftpd_connect_db off
ftpd_use_fusefs off
ftpd_use_passive_mode off
httpd_enable_ftp_server off
tftp_anon_write off
tftp_use_cifs off
tftp_use_nfs off

其中ftp_home_dir用于控制用户目录下的读写,如果此项是off则会报出 cannot change directory错误,使用命令开启

1
setsebool -P ftp_home_dir on

重启server

1
service vsftpd restart

重启完后程序可以正常连接登录。

MongoDB被Linux OOM Kill

今天无意发现线上mongodb集群中,某个mongodb连不上了,到服务器上看mongodb日志并未发现异常日志,就得过且过的想把它启起来就算了。
结果启动后大约1小时左右又突然消失,日志仍然没任何退出信息。。
这问题就大了,进程突然消失就像被kill -9了一样,但是查history记录并没人执行过。
于是 dmesg|grep mongo了一下果然发现问题

1
2
3
4
5
6
7
8
9
10
11
12
mongod invoked oom-killer: gfp_mask=0x201da, order=0, oom_adj=0, oom_score_adj=0
mongod cpuset=/ mems_allowed=0
Pid: 10123, comm: mongod Not tainted 2.6.32-431.el6.x86_64 #1
[ 3235] 0 3235 2568388 270165 6 0 0 mongod
[10113] 0 10113 1094500 749847 0 0 0 mongod
Out of memory: Kill process 3235 (mongod) score 252 or sacrifice child
Killed process 3235, UID 0, (mongod) total-vm:10273552kB, anon-rss:1080440kB, file-rss:220kB
[10113] 0 10113 1179731 454666 0 0 0 mongod
[32534] 0 32534 1549332 1316079 3 0 0 mongod
[32663] 0 32663 232939 37877 2 0 0 mongodump
Out of memory: Kill process 32534 (mongod) score 184 or sacrifice child
Killed process 32534, UID 0, (mongod) total-vm:6197328kB, anon-rss:5263596kB, file-rss:724kB

与mongodb日志对比发现,被杀的pid与最后一次启动进程的pid一致,可以确认被linux oom(Out of Memory) killer杀了。
而触发oom killer一般是应用程序大量请求内存导致系统内存不足造成的,而为了保证整个系统的稳定linux内核会杀掉某个进程。

Linux 内核根据应用程序的要求分配内存,通常来说应用程序分配了内存但是并没有实际全部使用,为了提高性能,这部分没用的内存可以留作它用,这部分内存是属于每个进程的,内核直接回收利用的话比较麻烦,所以内核采用一种过度分配内存(over-commit memory)的办法来间接利用这部分 “空闲” 的内存,提高整体内存的使用效率。一般来说这样做没有问题,但当大多数应用程序都消耗完自己的内存的时候麻烦就来了,因为这些应用程序的内存需求加起来超出了物理内存(包括 swap)的容量,内核(OOM killer)必须杀掉一些进程才能腾出空间保障系统正常运行。

接着往上看dmesg发现这段

1
2
3
4
5
6
7
8
9
10
11
12
Node 0 DMA: 1*4kB 1*8kB 1*16kB 1*32kB 1*64kB 1*128kB 0*256kB 0*512kB 1*1024kB 1*2048kB 3*4096kB = 15612kB
Node 0 DMA32: 505*4kB 347*8kB 230*16kB 175*32kB 122*64kB 86*128kB 48*256kB 16*512kB 8*1024kB 2*2048kB 0*4096kB = 65660kB
Node 0 Normal: 1227*4kB 1125*8kB 827*16kB 353*32kB 145*64kB 43*128kB 4*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 54244kB
7665 total pagecache pages
5466 pages in swap cache
Swap cache stats: add 16986448, delete 16980982, find 7091644/8157320
Free swap = 0kB
Total swap = 8241144kB
4194288 pages RAM
110410 pages reserved
1934 pages shared
4043507 pages non-shared

可以确定mongodb进程的消失是因为swap耗尽导致。而swap的耗尽又可能是因为业务的必要机制需要当新集群服务上线从而进行集中的整库的查询导致swap耗尽。

通过

1
cat /proc/meminfo


1
free -m

看了下,机器硬件条件确实一般,内存和swap也所剩不多。

给进程按内存排个序

1
ps -e -o 'pid,comm,args,rsz,vsz'|sort -nrk4

前三甲的大户都是pid小于5k的一等公民,也不敢动,自然pid五位数又能吃内存的mongodb被选中kill。要是我也选它。

触发oom killer后选择哪个进程被杀,是根据内核特定的算法给每个进程打分从而决定是否被选中,分数可以在

1
/proc/$pid/oom_score

中看到,而设置oom_adj的值可以调整oom killer的行为,比如

1
echo -17 > /proc/$pid/oom_adj

oom_adj的可调值为15到-16,其中15最大-16最小,-17为禁止使用oom,值越小越不容易被杀。

虽然问题找到,但解决起来却很纠结,oom killer虽然杀掉mongodb进程,但它也是为了保证整个系统稳定,即使调整mongodb的分数,但最终也会有一个进程被选中杀掉。如关闭oom killer当系统资源耗尽可能导致的结果就是系统无响应需要重启,并且只是将问题暂时的盖住而已,问题仍然在那不符合fast fail的理念。

serverfault上有个不错的oom killer实践可以参考的看看。

OOM killer is not a way anyone manages memory; it is Linux kernels way to handle fatal failure in last hope to avoid system lockup!

What you should do is:

make sure you have enough swap. If you are sure, still add more.
implement resource limits! At LEAST for applications you expect that will use memory (and even more so if you don’t expect them to - those ones usually end up being problematic). See ulimit -v (or limit addressspace) commands in your shell and put it before application startup in its init script. You should also limit other stuff (like number of processes -u, etc)… That way, application will get ENOMEM error when there is not enough memory, instead of kernel giving them non-existent memory and afterwards going berserk killing everything around!
tell the kernel not to overcommit memory. You could do:

echo “0” > /proc/sys/vm/overcommit_memory
or even better (depending on your amount of swap space)

echo “2” > /proc/sys/vm/overcommit_memory; echo “80” > /proc/sys/vm/overcommit_ratio
See Turning off overcommit for more info on that.

That would instruct kernel to be more carefull when giving applications memory it doesn’t really have (similarity to worlds global economic crisis is striking)
as a last dich resort, if everything on your system except MangoDB is expendable (but please fix two points above first!) you can make lower the chances of it being killed (or even making sure it won’t be killed - even if alternative is hangup machine with nothing working) by tuning /proc/$pid/oom_score_adj and/or /proc/$pid/oom_score.

echo “-1000” > /proc/pidof mangod/oom_score_adj
See Taming the OOM killer for more info on that subject.

API安全认证技术(翻译)

在网上看到的一个API安全认证技术对比表格,觉得蛮好的,方案选型时用的上,MARK并翻译一下。

原文来自:Authentication Techniques for APIs

Hexo的表格渲染太烂了。。。还是看原文吧

           HTTP Basic Auth Stateless Session Cookie JWT Stateful Session Cookie Random Token Full Request Signature OAuth
简介 基础认证,需要每个请求都附带用户名与密码 对包含用户信息的cookie进行签名或加密,通常Web框架可以处理 对用户信息签名或加密到一个编码后的json字符串,每个语言都有经过良好测试的类库可以利用 标准cookie,大部分web框架和浏览器支持 一个不包含任何信息 强壮、安全的随机令牌,无法被猜测。等价于session id 通过AWS认证普及开来的一种机制。客户端与服务端共享一个秘钥,客户端使用这个秘钥对整个请求进行签名,服务端通过这个秘钥对请求进行验证。AWS文档 当你想从第三方应用中获取你用户的信息,可以使用这种机制。如果你没有第三个应用,oauth机制则有点大材小用
适用场景 最好仅用在内网没什么价值的服务端接口认证 如果仅仅开发一个基于web的应用,并且你的框架支持,而且你没有一个类似Redis/Memcache的分布缓存,那么可以用用,但不要自己实现(利用框架或容器) 当你可以放弃自动过期与主动失效机制时,原生移动端,web移动端,服务端适用 适用于可以把信息存储到数据库或分布式缓存中的web应用 如果你有一个类似redis或memcache的分布式缓存那么web应用与移动应用适用,服务端更适合基于JWT的一次性令牌 当你为你的客户端提供管理加解密算法的类库并且在意重放攻击的服务端应用适用。实际上,为每个请求使用JWT并且将url和关键请求参数包含到JWT中,这种方法最大的好处是不用实现复杂的标准算法 适用于你要从外部第三方应用获取数据并且你的用户授权第三方应用提供他的数据给你的场景,如果不是,oauth有点大材小用
存储机制 用户名密码存储在服务端。用户名密码附带在每个请求上,服务端通过传递过来的用户名密码校验用户是否合法,校验通过后进行请求对应的操作 存在cookie中 编码后的json(令牌)中 服务端的内存、数据库、分布式缓存或磁盘中 服务端的内存、数据库、分布式缓存或磁盘中 服务端,userid通过请求传递,服务器通过签名确定用户,然后从数据库中获取想要的信息 TBD
过期策略 无,服务器端管理 有,cookie控制 有,令牌控制 有,服务端控制 有,服务端控制 通过请求传递,通常仅设置一个很短的时间 TBD
加密机制 有,通常web框架处理 有,有成熟的第三方类库 有,但是AWS没有提供类库,可以提供一个自己的客户端类库,否则很痛苦 TBD
闲置过期(失效/超时) 无,不要将此技术用于基于Web的客户端 服务端需要为每个请求的cookie设置新的超时时间,如果web框架不支持,自己处理会很痛苦 有很痛苦,你需要刷新客户端提供令牌,并且要在数据库或缓存中维护令牌最后一次请求时间,而且已然失去无状态的好处 有,每个web框架都以实现 有,每个进来的请求在服务端判断超时时间 无意义 TBD
主动失效 无,不要将此技术用于基于Web的客户端(前面直翻,其实依赖服务端) 很痛苦,需要在一个分布式存储中维护一个撤销列表。这样已然失去无状态的好处 很痛苦,需要在一个分布式存储中维护一个撤销列表。这样已然失去无状态的好处 有,实际上取决于使用的web框架 有,在服务端删除生成的令牌那么下个进来的请求将被认为未授权的请求 有,服务端可以请求拒绝请求 TBD
浏览器存储 无,不要将此技术用于基于Web的客户端 浏览器自动存储,程序无需处理 有,开发人员需要编码存储token。通常是sessionStorage或localStorage中 浏览器自动存储,程序无需处理 有,开发人员需要编码存储token。通常是sessionStorage或localStorage中 不适用,此机制不适合web应用 TBD
凭证传递 通过HTTP头 Authorization传递。
Example:
Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l 用户名密码没有被加密
通过cookie传递,浏览器自动发送到服务器 通过请求头Authorization: Bearer <token>传递,开发人员需要编码为每个请求添加 通过cookie传递,浏览器自动发送到服务器 通过请求头Authorization: Bearer <token>传递,开发人员需要编码为每个请求添加 通过请求头Authorization传递,但是适用自定义的schema替换了”bearer”。开发人员需要编码为每个请求添加 TBD
CSRF预防 非常脆弱,不要用在基于Web应用,除非是非常没价值的内网web应用 非常脆弱,开发者需要自己预防跨站脚本攻击 可以预防,请求头中包含token 非常脆弱,开发者需要自己预防跨站脚本攻击 可以预防,请求头中包含token 不适用,此机制不适合web应用 TBD
移动端适用 不要使用,web应用无法安全的存储凭证,同时用户也不会想手动的为每个请求输入凭证 原生移动端会很痛苦,避免为原生移动端基于session认证的api 移动端用户期望只需登录一次,JWT通过设置一个长的失效时间可以实现,但是更好的策略是用一个不存储任何信息的安全随机令牌 原生移动端会很痛苦,避免为原生移动端基于session认证的api 在第一次登录的时候创建一个随机令牌,在app的整个生命周期中使用它 不适用,此机制不适用web应用 TBD
服务端适用 如果服务端应用(内网相互调用的服务)基于HTTPS,基础认证也许是个不错(方便)的选择。基础认证使用非常简单,客户端与服务端框架的支持非常好 服务端应用会很痛苦,避免为服务端提供基于session认证的api 创建一个公私秘钥对,然后让客户端使用私钥生成JWT,服务端通过公钥进行验证。也可以维护一个共享的口令来创建JWT。当然公私秘钥对最佳。为每个请求创建JWT,同时设置一个很短的过期时间 服务端应用会很痛苦,避免为服务端提供基于session认证的api 类似客户端传递api key,不过如果其他人获取了api key有可能造成重放攻击。服务端最好使用JWT公私秘钥对 如果服务端需要更高的安全性这是首选,原则上类似JWT共享秘钥,但在此机制下所有的东西都会被签名:url、请求参数、请求头、请求体 TBD
重放攻击预防 绝对有可能 有可能 如果过期时间很长,那很有可能,如果每个请求的JWT只用一次那么重放会变的很难 有可能 有可能 所有东西都被签名了,不可能重放 TBD

Jersey开启Gzip踩的坑

最近给一个基于Jerysey的Restful项目增加gzip功能,通过自动代码提示发现Jersey自带了一个GZipEncoder,通过注释看,只要头CONTENT_ENCODING为gzip或x-gzip就可以进行压缩和解压。
于是在ResourceConfig里直接注册了GzipEncoder。

1
register(GzipEncoder.class);

但在测试的时候发现,请求压缩后decode没问题,但返回的响应则无法encode,也就是无法压缩,一开始以为是ACCEPT_ENCODING问题,但设置了也不对,最后debug发现根本进不去GzipEncoder#encode方法。
测试代码:

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
CloseableHttpClient client = HttpClients.createDefault();
StringEntity entity = new StringEntity("{\"id\":\"123\"}", ContentType.create(
ContentType.APPLICATION_JSON.getMimeType(), "UTF-8"));
entity.setContentEncoding("UTF-8");
GzipCompressingEntity gc=new GzipCompressingEntity(entity);

String out = null;
try {
// send
HttpPost post = new HttpPost("http://127.0.0.1:8080/services/test/v1/x");
try {
post.addHeader("Accept-Encoding","gzip");
post.setEntity(gc);
CloseableHttpResponse response = null;
try {
response = client.execute(post);
int code = response.getStatusLine().getStatusCode();
if (code != HttpStatus.SC_OK) {
throw new BizSystemException("发送请求失败,返回的状态码:" + code);
}
HttpEntity respEntity = response.getEntity();

// consume
out = EntityUtils.toString(respEntity, "UTF-8");
EntityUtils.consume(respEntity);
} catch (Exception e) {
log.error("发送请求异常", e);
throw new BizSystemException("发送请求失败", e);
} finally {
IOUtils.closeQuietly(response);
}
} finally {
post.releaseConnection();
}
} finally {
IOUtils.closeQuietly(client);
}

最后google了一下发现启用GzipEncoder压缩响应属于entity-filtering,需要开启entity-filtering,于是引入类库:

1
2
3
4
5
<dependency>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-entity-filtering</artifactId>
<version>2.21</version>
</dependency>

并且开启entity-filtering功能,以及激活gzipencoder:

1
2
register(EntityFilteringFeature.class);
EncodingFilter.enableFor(this, GZipEncoder.class);

通过日志看已经可以正确处理请求和响应的压缩。
使用时还需要注意的时GZipEncoder的优先级为:

1
2
3
4
@Priority(Priorities.ENTITY_CODER)
public class GZipEncoder extends ContentEncoder{
...
}

也就是4000,如果有自定义的过滤器/拦截器需要注意下顺序。

比较坑的是看源码GZIPEncoder仅仅是一个拦截器,按照以往的经验注册一下调整下顺序就应该是开箱即用的,为毛会和Entity-Filtering扯上关系,还要多引一个依赖,根据官网Entity-Filtering的介绍来看:

Support for Entity Filtering in Jersey introduces a convenient facility for reducing the amount of data exchanged over the wire between client and server without a need to create specialized data view components. The main idea behind this feature is to give you APIs that will let you to selectively filter out any non-relevant data from the marshalled object model before sending the data to the other party based on the context of the particular message exchange. This way, only the necessary or relevant portion of the data is transferred over the network with each client request or server response, without a need to create special facade models for transferring these limited subsets of the model data.

Entity-Filtering功能最大的作用就是减少传输间的显示组件(view components),也就是说往往在客户端与服务器端的数据交换都需要定义很多JavaBean用来序列化请求/响应,哪怕有很多无关的数据需要组合到一起都要定义一个根的JavaBean用于组合它们(Facade模式)。

Entity-Filtering可以在传输的时候对数据进行处理,避免创建这些Facade对象。

所以究竟为啥一个拦截器的工作需要和Entity-Filtering有关联呢。😂

参考:

1.how-to-compress-responses-in-java-rest-api-with-gzip-and-jersey

TCP优化--减少TIME_WAIT提高吞吐

近期做压力测试的时候发现有很多TIME_WAIT,虽然程序吞吐达标,不过还是想进一步的优化下。
由于一台服务器建立的连接数是有限的,也就65535,外加一些不能用的,比如0-1024和其他程序占的。
可以使用命令查看当前服务器可以使用的端口范围,比如目前这台CentOS 6.5

1
2
3
4
5
6
7
lsb_release -a

LSB Version: :base-4.0-amd64:base-4.0-noarch:core-4.0-amd64:core-4.0-noarch:graphics-4.0-amd64:graphics-4.0-noarch:printing-4.0-amd64:printing-4.0-noarch
Distributor ID: CentOS
Description: CentOS release 6.5 (Final)
Release: 6.5
Codename: Final

使用命令:

1
cat /proc/sys/net/ipv4/ip_local_port_range

输出

1
32768	61000

代表当前服务器可用端口数为61000-32768=28232,也就是说理论上最多建立的连接就是28232。

在做压力测试的时候,通过命令统计TIME_WAIE的数量为2w+

1
netstat -an |grep TIME_WAIT|wc -l

使用命令增大端口范围

1
echo "5000 65535">/proc/sys/net/ipv4/ip_local_port_range

再次做压力测试时候TIME_WAIT的数量下降的80+。
除了调大端口范围,还可以设置SO_REUSEADDR、TCP连接复用进一步优化。