ScheduledExecutorService 一个小坑

今天在排查一个线上问题是发现一个使用 ScheduledExecutorService 执行的定时任务在执行了一次以后就再也没有执行过了。于是 Dump 了内存来检查问题。

首先,搜索对应 Task 的类,发现在堆中找不到这个类的实例。可是明明已经成功执行了一次,为何没有实例?

于是再去找 ScheduledExecutorService  对应的 ScheduledThreadPoolExecutor  类,成功筛选出了用来执行定时任务的实例。在实例的 queue 中,却只看到了 6 个 Task 对象,唯独不见了这个出问题的对象。百思不得解,因为日志中这个对象的 Logger 已经打印出来了,说明至少执行了一次,为啥会从内存中消失呢?

在同事的帮助下,查阅了 API 文档,发现了这么一句话:

If any execution of the task encounters an exception, subsequent executions are suppressed. Otherwise, the task will only terminate via cancellation or termination of the executor. If any execution of this task takes longer than its period, then subsequent executions may start late, but will not concurrently execute.

注意通常的理解,这里的 suppressed 意思应该为抑制、压制,一般意义上理解为可能是说降低频率啊权重啊什么的。可是实际上,这里使用 stoped 更合适。从表现上看,你的 Task 只要出现了异常,就会被彻底扔掉,再也不会执行。

下面给出一个网上同样问题的复现代码:

import java.util.concurrent.Executors;

public class BadAssTask implements Runnable {

        @Override
        public void run() {
                System.out.println("Sleeping ...");
                try {
                        Thread.sleep(100);
                } catch (InterruptedException e) {
                        e.printStackTrace();
                }
                System.out.println("Throwing ... ");
                throw new RuntimeException("bad ass!");
        }

        public static void main(String[] args) {
                Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(new BadAssTask(), 1, 1, TimeUnit.SECONDS);
        }

}

当我们注释掉 throw new RuntimeException(“bad ass!”);  的时候,可以看到每个 0.1s 会有一行 Sleeping … 输出。当开启注释时,Throwing … 之后再也没有 Sleeping 输出了。

于是来查看对应的代码,在 ScheduledThreadPoolExecutor  的代码中看到执行 Task 的实际调用方法为:

/**
 * Overrides FutureTask version so as to reset/requeue if periodic.
 */
public void run() {
    boolean periodic = isPeriodic();
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    else if (!periodic)
        ScheduledFutureTask.super.run();
    else if (ScheduledFutureTask.super.runAndReset()) {
        setNextRunTime();
        reExecutePeriodic(outerTask);
    }
}

注意最后一个 if 语句。当 Task 执行出错的时候, runAndReset  的返回值为 False,所以 if 里面的内容不会执行,因此这个 task 就不会被放回队列,也就再也不会被执行了。

runAndReset 方法的代码如下:

/**
 * Executes the computation without setting its result, and then
 * resets this future to initial state, failing to do so if the
 * computation encounters an exception or is cancelled.  This is
 * designed for use with tasks that intrinsically execute more
 * than once.
 *
 * @return {@code true} if successfully run and reset
 */
protected boolean runAndReset() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return false;
    boolean ran = false;
    int s = state;
    try {
        Callable c = callable;
        if (c != null && s == NEW) {
            try {
                c.call(); // don't set result
                ran = true;
            } catch (Throwable ex) {
                setException(ex);
            }
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
    return ran && s == NEW;
}

注意其中的 setException 方法。这个方法会把 state 设置成出现异常的状态:

/**
 * Causes this future to report an {@link ExecutionException}
 * with the given throwable as its cause, unless this future has
 * already been set or has been cancelled.
 *
 * 

This method is invoked internally by the {@link #run} method * upon failure of the computation. * * @param t the cause of failure */ protected void setException(Throwable t) { if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { outcome = t; UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state finishCompletion(); } }

于是在 runAndReset 最后的判断中,s == NEW 不成立,于是返回 False 了。

参考:

http://code.nomad-labs.com/2011/12/09/mother-fk-the-scheduledexecutorservice/

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ScheduledExecutorService.html#scheduleAtFixedRate-java.lang.Runnable-long-long-java.util.concurrent.TimeUnit-

记一次 JNI 导致 Java fd 泄露的排查过程

前几天接到反馈,线上某机器上的服务在进行后端调用的时候失败了。查看日志是端口分配失败。通过 netstat -nulp  看到大量端口占用,用户段端口49152 到 65535 全部被占满。于是通过 awk sort 和 uniq 统计出每个进程的端口占用情况,发现某些 Java 服务占用了 2w+ 端口,于是对该服务展开分析。

首先考虑的是应用有大量 Socket 对象没有关闭释放,于是将堆 dump 出来,使用 VisualVM 加载分析。由于泄露的是 UDP 端口,于是考虑查找 Java 中 UDP socket 对应的对象 DatagramSocket 。可是一顿操作之后发现堆中并不存在该类对象,倒是找到了几个 DatagramSocketAdaptor  对象。查看了框架发现使用了 Netty 做 NIO,因此实际使用的是 NioDatagramChannel 和 DatagramSocketAdaptor 对象。经过对比,这些对象都是应用启动时创建 Netty channel 时创建的,数量完全正确,所以排除这方面的问题。

接着考虑 Netty 版本和 JVM 版本的问题。通过搜索发现 Netty 和 JVM 都出现过 fd 没有正确关闭的问题,于是进行升级。升级后发现问题依然存在,排除相关组件的问题。

在排除了几个可能之后,还是没有什么头绪,于是准备从 FD 本身入手。在 Java 中,所有的系统 FD 对应的都是一个 Java 中的 FileDescriptor  类,于是使用各种工具对该类的创建和删除进行了监控。结果通过排查发现,除了第一步中查到的 Netty Channel 持有的 fd 实例外,没有任何的有关网络 Socket 的 fd 实例。这下就很尴尬了,JVM 中的 fd 没有变化,按理说应该不会有 Socket 创建才对。仔细思考了一下,想到了最后一种可能,那就是 native 方法。

在 Java 中,为了支持调用其它语言所编写的方法,提供了 Java Native Interface 即 JNI 作为入口。当我们需要调用一个闭源的 C++ 编写的类库的时候,我们就可以使用 JNI 来进行调用。同时,由于 JNI 实际执行的是第三方类库中的代码,因此这部分代码进行的 fd 操作都不会被 JVM 管理,自然也就不会出现在 Dump 文件中。

既然猜到了可能的问题,接下来就需要排查了。可是由于一些原因,该服务中存在很多的 JNI 类库都会进行网络调用,无法最终确定。于是想了这么一个办法来进行排查:

  1. 在机器上起一个程序不断的对 lsof -a -n -P -i udp -p pid  进行采样对比,遇到两次采样期间有新增的 fd 就打印出来
  2. 同时使用 tcpdump -i any udp -w fd_leak.pcap  进行全量抓包,记录机器上所有的 UDP 网络流量,来对比分析流量发出的进程

经过排查,终于抓到了对应的包,找到了对端的 IP 端口,定位到了对应的组件。于是对这个组件编写复现代码进行测试,最终将代码简化为:

package com.maoxian.test;

import com.maoxian.xxx.NativeAPI;

/**
 * 测试类
 */
public class Test {
    public static void main(String[] args) {
        Thread.sleep(10000);
        int i = 0;
        while (i < 10) {
            i++;
            System.out.println("Start loop " + j + " ...");
            new Thread() {

                @Override
                public void run() {
                    try {
                        System.out.println("Thread " +
                                Thread.currentThread().getId() +
                                " is running...");


                        NativeAPI.doSomething();
                    } finally {
                        System.out.println("Thread " +
                                Thread.currentThread().getId() +
                                " finished.");
                    }
                }
            }.start();
            System.out.println("Finish loop " + j + ".");
        }
        Thread.sleep(60000);
    }
}

这段代码的作用非常简单,就是起 10 个线程去调用这个组件,每个线程调用一次就退出。Sleep 的作用是给 lsof 列出 fd 预留时间。在执行这段代码后可以明显的看到 fd 在 Native 方法调用时分配,但是在线程退出后没有释放。咨询了相关同事后得知,由于在 C++ 中,一般不会使用 Java 中的这种线程池模型,而是使用固定线程模型。当一个线程退出的时候通常意味着整个程序的退出。所以这个组件在制作的时候只考虑了线程安全的问题对每个线程独立分配了资源,但是没有考虑线程终止时候的资源释放

在定位到 fd 泄露与线程创建相关后,对相应的业务框架代码进行了分析。发现框架中创建线程池使用的是:

this.tasks = Executors.newCachedThreadPool(new ThreadFactory() {
    final AtomicInteger TID = new AtomicInteger(0);
    final String GROUP_NAME = "TPWS-" + GID.getAndIncrement();
    @Override
    public Thread newThread(Runnable r) {
        return new Thread(r, GROUP_NAME + TID.getAndIncrement());
    }
});

查看 Executors 的创建线程池的方法:

/**
 * Creates a thread pool that creates new threads as needed, but
 * will reuse previously constructed threads when they are
 * available, and uses the provided
 * ThreadFactory to create new threads when needed.
 * @param threadFactory the factory to use when creating new threads
 * @return the newly created thread pool
 * @throws NullPointerException if threadFactory is null
 */
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue(),
                                  threadFactory);
}

可以看出,这个方法最终创建了一个最小 0 个线程,最大 Integer.MAX_VALUE 个线程,每个线程 60s 超时时间。于是,当一个业务请求到来时,线程池创建一个新线程处理这个请求,在请求中调用了 Native 方法。当请求处理完后,线程归还线程池。60s 内如果没有新的请求进入,则该线程被线程池销毁,但是 Native 申请的 fd 没有释放。当新的请求到来时,又重新创建线程,重新分配 fd,以此类推,导致 fd 泄露。

这次排查的经理告诉我们,当 Java 调用 Native 方法的时候一定要格外小心。由于虚拟机无法对大多数 Native 方法占用的资源进行管理,因此编写质量差的 Native 类库会直接导致不可预知的奇怪问题。特别是由于 C++ 在多线程、多进程模型上与 Java 有诸多不同,很多库在设计时就没有考虑全面,直接在 Java 内调用的时候可能就会出现在 C++ 中不会遇到的问题。

Tomcat 在处理 Cookie 的时候的几个小坑

今天在代码中调用 HttpServletRequest  对象的 getCookies()  方法时,发现实际得到的 Cookie 数量与提交的不符。实际提交了 17 个 Cookie,但是获取到的只有 14 个。

经过排查,发现如果调用 getHeaders(“Cookie”)  方法,获取原始的 Cookie 串,是可以拿到正确的 17 个 Cookie 组成的字符串的。于是确认应该是 Tomcat 在处理 Cookie 的时候进行了过滤。

经过一番搜索,发现了这个文档:

http://tomcat.apache.org/tomcat-7.0-doc/config/systemprops.html

其中,跟 Cookie 相关的参数有:

org.apache.tomcat.util.http. ServerCookie.ALLOW_EQUALS_IN_VALUE If this is true Tomcat will allow ‘=‘ characters when parsing unquoted cookie values. If false, cookie values containing ‘=‘ will be terminated when the ‘=‘ is encountered and the remainder of the cookie value will be dropped.

If not specified, the default value specification compliant value of false will be used.

org.apache.tomcat.util.http. ServerCookie.ALLOW_HTTP_SEPARATORS_IN_V0 If this is true Tomcat will allow HTTP separators in cookie names and values.

If not specified, the default specification compliant value of false will be used.

org.apache.tomcat.util.http. ServerCookie.ALLOW_NAME_ONLY If this is false then the requirements of the cookie specifications that cookies must have values will be enforced and cookies consisting only of a name but no value will be ignored.

If not specified, the default specification compliant value of false will be used.

org.apache.tomcat.util.http. ServerCookie.ALWAYS_ADD_EXPIRES If this is true Tomcat will always add an expires parameter to a SetCookie header even for cookies with version greater than zero. This is to work around a known IE6 and IE7 bug that causes IE to ignore the Max-Age parameter in a SetCookie header.

If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to true, the default of this setting will be false, else the default value will be true.

org.apache.tomcat.util.http. ServerCookie.FWD_SLASH_IS_SEPARATOR If this is true then the / (forward slash) character will be treated as a separator. Note that this character is frequently used in cookie path attributes and some browsers will fail to process a cookie if the path attribute is quoted as is required by a strict adherence to the specifications. This is highly likely to break session tracking using cookies.

If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to true, the default of this setting will be true, else the default value will be false.

org.apache.tomcat.util.http. ServerCookie.PRESERVE_COOKIE_HEADER If this is true Tomcat will ensure that cookie processing does not modify cookie header returned by HttpServletRequest.getHeader().

If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to true, the default of this setting will be true, else the default value will be false.

org.apache.tomcat.util.http. ServerCookie.STRICT_NAMING If this is true then the requirements of the Servlet specification that Cookie names must adhere to RFC2109 (no use of separators) will be enforced.

If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to true, the default of this setting will be true, else the default value will be false.

看了一下相关参数,跟我这个现象有关的是这两个选项:

org.apache.tomcat.util.http. ServerCookie.ALLOW_HTTP_SEPARATORS_IN_V0
org.apache.tomcat.util.http. ServerCookie.ALLOW_NAME_ONLY

于是按照这个选项去搜索,发现了这样一篇文章:

http://www.cnblogs.com/princessd8251/articles/4172103.html

其中详细说明了第一个选项的作用:

在网上查了下,cookie有以下版本:

版本0:由Netscape公司制定的,也被几乎所有的浏览器支持。Java中为了保持兼容性,目前只支持到版本0,Cookie的内容中不能包含空格,方括号,圆括号,等于号(=),逗号,双引号,斜杠,问号,@符号,冒号,分号。

版本1:根据RFC 2109(http://www.ietf.org/rfc/rfc2109.txt)文档制定的. 放宽了很多限制. 上面所限制的字符都可以使用. 但为了保持兼容性, 应该尽量避免使用这些特殊字符。

Tomcat 具体的过滤源码里面包含了这些字符:

如果 org.apache.tomcat.util.http.ServerCookie.FWD_SLASH_IS_SEPARATOR 设为 true (默认 False),’/’也会被作为http分隔符

如果 org.apache.tomcat.util.http.ServerCookie.ALLOW_HTTP_SEPARATORS_IN_V0 设为 true (默认 False),tomcat将会允许cookie的name和value使用http分隔符

tomcat源码中的http分隔符:’\t’, ‘ ‘, ‘\”‘, ‘(‘, ‘)’, ‘,’, ‘:’, ‘;’, ‘<', '=', '>‘, ‘?’, ‘@’, ‘[‘, ‘\\’, ‘]’, ‘{‘, ‘}’

然后第二个选项就很好理解了,如果 Cookie 的 Value 是空,那么直接干掉。

于是,我们传的 Cookie 是这样的:

aaa=@abcdefg

Tomcat 先做了一次截断,将 @ 和后面的内容都去掉了;然后做了一次判断,发现 aaa 这个 Cookie 只有 name 没有 value,于是直接干掉了。

到此真相大白。

 

如果想要解决这个问题,有两个方法:

1. 在 Tomcat 的配置文件 catalina.properties  中设置 org.apache.tomcat.util.http.ServerCookie.ALLOW_HTTP_SEPARATORS_IN_V0=true

2. 在代码中使用 request.getHeader(“Cookie”) 取出原始的 Cookie 串,自行处理。

MySQL 低版本对 TIMESTAMP 字段 DEFAULT 值设置的一个问题

今天在线上改表的时候,遇到了一个问题。改表完成之后,MyBatis Select 改表前插入的数据会抛异常。排查后发现是因为一个 TIMESTAMP  类型的字段值被写成了 0000-00-00 00:00:00  ,无法被转换成 java 的 java.sql.Timestamp 类型,导致出错。

但是之前在使用 MySQL 的时候一直没有遇到过这个问题,于是搜索了一下,发现了这么一个链接:

https://bugs.mysql.com/bug.php?id=68040

这里反馈了一个 MySQL 本身的 Bug,会导致 ALTER table  的时候,TIMESTAMP  类型的值没有按照预期的正确设置,而是被设置成了 0000-00-00 00:00:00  。该问题在 MySQL 5.6.11 中被修复了。官方的 Release Note 在这里:

https://dev.mysql.com/doc/relnotes/mysql/5.6/en/news-5-6-11.html#mysqld-5-6-11-bug

  • ALTER TABLE tbl_name ADD COLUMN col_name TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP inserted 0000-00-00 00:00:00 rather than the current timestamp if the alteration was done in place rather than by making a table copy. (Bug #68040, Bug #16076089)

为了确认该问题,特地找了两台不同版本的 MySQL 来尝试一下:

没有问题的版本:

mysql> SHOW VARIABLES LIKE "%version%";
+-------------------------+--------------------+
| Variable_name           | Value              |
+-------------------------+--------------------+
| innodb_version          | 5.6.28             |
| protocol_version        | 10                 |
| slave_type_conversions  |                    |
| version                 | 5.6.28             |
| version_comment         | 20170228           |
| version_compile_machine | x86_64             |
| version_compile_os      | Linux              |
+-------------------------+--------------------+
7 rows in set (0.01 sec)

mysql> ALTER TABLE time_test ADD COLUMN update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> SELECT * FROM time_test;
+----+------+---------------------+
| id | name | update_time         |
+----+------+---------------------+
|  1 | 111  | 2018-01-01 00:00:01 |
|  2 | 222  | 2018-01-01 00:00:01 |
+----+------+---------------------+
2 rows in set (0.01 sec)

mysql>

有问题的版本:

mysql> SHOW VARIABLES LIKE "%version%";
+-------------------------+------------------------------+
| Variable_name           | Value                        |
+-------------------------+------------------------------+
| innodb_version          | 1.1.8                        |
| protocol_version        | 10                           |
| slave_type_conversions  |                              |
| version                 | 5.5.24                       |
| version_comment         | MySQL Community Server (GPL) |
| version_compile_machine | x86_64                       |
| version_compile_os      | Linux                        |
+-------------------------+------------------------------+
7 rows in set (0.07 sec)

mysql> ALTER TABLE time_test ADD COLUMN update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
Query OK, 4 rows affected (0.06 sec)
Records: 4  Duplicates: 0  Warnings: 0

mysql> SELECT * FROM time_test;
+----+------+---------------------+
| id | name | update_time         |
+----+------+---------------------+
|  1 | 111  | 0000-00-00 00:00:00 |
|  2 | 222  | 0000-00-00 00:00:00 |
|  3 | 333  | 0000-00-00 00:00:00 |
|  4 | 444  | 0000-00-00 00:00:00 |
+----+------+---------------------+
4 rows in set (0.09 sec)

mysql>

 

Nginx 诡异 SSL_PROTOCOL_ERROR 问题排查

这两天在检查一台 Nginx 配置的时候,遇到了一个极端诡异的问题。一段很通用的配置,配在这个服务器上,就会 100% 导致 Chrome 报 ERR_SSL_PROTOCOL_ERROR 。但是这段配置非常的通用,是用 Mozilla 提供的工具生成的。

而且在 iPhone 的 Safari 上访问又是完全正常的,服务器日志也看不到任何错误。看到的请求相应码也是完全正确的 200 。

先贴出配置:

# https://mozilla.github.io/server-side-tls/ssl-config-generator/
    listen 443 ssl http2;

    # certs sent to the client in SERVER HELLO are concatenated in ssl_certificate
    ssl_certificate /path/to/signed_cert_plus_intermediates;
    ssl_certificate_key /path/to/private_key;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;


    # modern configuration. tweak to your needs.
    ssl_protocols TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
    ssl_prefer_server_ciphers on;

可以看到是在 Mozilla 网站上选择  和 Modern 生成出来的配置。

在测试过程中,排查了各种问题,包括但不限于 SSL 证书问题,HTTP Basic Auth 问题,http2 问题等等,然而都没有解决这个现象。

一次偶然的尝试,发现只要注释掉我给这个 server 特殊配置的这段逻辑,使用服务器通用的 ssl.ngx 文件中的 SSL 配置,就不会出现问题。于是开始先使用 ssl.ngx 文件中的配置,然后逐行替换,来查找具体出现问题的配置。

终于,当我将配置中的这行加上时,问题出现了:

ssl_session_tickets off;

于是以这个配置作为关键字搜索,找到了这么一篇文章:

https://community.letsencrypt.org/t/errors-from-browsers-with-ssl-session-tickets-off-nginx/18124/5

I’m posting this here both because this question was recently asked and because it took me many hours of troubleshooting to figure out the issue as while I found several references to the problem on Google, no one seemed to have a real solution. So here it is:

ssl_session_tokens off breaks if it’s not set the same for all ssl-enabled server{} blocks. So if you have 2 server configurations and and you have ssl_server_tokens set to on in one (which is the default so it counts even if you omit it) and set to off in another, it will break the one where it’s set to off in certain browsers. The easiest way to resolve this, unless you have multiple http{} blocks, is to just set it to off in the http{} block. I have not tested to see if you you can have different settings in different http{} blocks as I haven’t had need to set up more than one http{} block.

For others looking for this issue, I want to add that Chrome will respond with: ERR_SSL_PROTOCOL_ERROR while Firefox responds with: SSL_ERROR_RX_UNEXPECTED_NEW_SESSION_TICKET and curl responds with: gnutls_handshake() failed: An unexpected TLS packet was received. IE seemed to work, surprisingly.

简单翻译一下,这里是说,如果你的 nginx 开了多个 https 的 server,其中某些 server 没有配置 ssl_server_tokens off; ,而有些 server 配置了这个选项,那么就会导致没有手动 off 的 server 采用默认值 on,而手动 off 掉的 server 采用 off。这种情况会导致 nginx 和浏览器之间的握手出现问题,从而导致 Chrome 报出 ERR_SSL_PROTOCOL_ERROR ,FireFox 则会报出 SSL_ERROR_RX_UNEXPECTED_NEW_SESSION_TICKET 。

那么解决方法也很简单,只要在所有的 server 块统一这个配置就好了。要么都设置为 on,要么都设置为 off,问题解决。目前没有尝试多个 http 块隔离两个 server,建议还是将这个配置统一一下。

 

 

常见 NAT 类型描述对应关系

在不同的设备上,对于各种类型的 NAT 有各自不同的描述。先列举一下目前遇到的说法:

PS4

NAT类型 显示PS4™与互联网的连接方式
使用游戏的通信功能等时,可确认与其它PS4™间的连接稳定性。
Type 1:与互联网直接连接。
Type 2:通过路由器与互联网连接。
Type 3:通过路由器与互联网连接。
显示Type 3时,可能会出现无法与其它PS4™顺利通信,使PS4™的网络功能受到限制的情形。详细请参阅

XBox

OPEN NAT MODERATE NAT STRICT NAT
With an OPEN NAT type, you’re able to chat with other people, as well as join and host multiplayer games with people who have any NAT type on their network. With a MODERATE NAT type, you’re able to chat and play multiplayer games with some people; however, you might not be able to hear or play with others, and normally you won’t be chosen as the host of a match. With a STRICT NAT type, you’re only able to chat and play multiplayer games with people who have an OPEN NAT type. You can’t be chosen as the host of a match.

PC

NatTypeTester

Full Cone

Restricted Cone

Port Restricted Cone

Symmetric

其中的对应关系为:

Platform Great Not Good Bad
PS4, PS3 NAT Type 1  NAT Type 2 NAT Type 3
Xbox One, 360 NAT Type Open NAT Type Moderate NAT Type Strict
PC Full Cone Restricted Cone

Port Restricted Cone

Symmetric

未完,后面会介绍具体的信息。

MySQL 分区表的一些问题

最近在使用 MySQL 分区表的时候,研究了一下多列 Range 分区,也就是

PARTITION BY RANGE COLUMNS(`a`, `b`, `c`) (
    PARTITION p1 VALUES LESS THAN (0, 0, MAXVALUE),
    PARTITION p2 VALUES LESS THAN (10, 10, MAXVALUE),
    PARTITION p3 VALUES LESS THAN (20, 20, MAXVALUE)
)

在多列的情况下,MySQL 的分区策略和单列略有不同,这也是比较坑的地方,查遍所有文档都没人提到。。。

先说说单列 Range 分区。比如,如果这么写:

PARTITION BY RANGE(`a`) (
    PARTITION p1 VALUES LESS THAN (0),
    PARTITION p2 VALUES LESS THAN (10),
    PARTITION p3 VALUES LESS THAN (20)
)

那么,p1 中的数据是 a 值小于 0 的,注意,是小于,不包括 0 。然后,p2 中的数据是 a 值在 [0, 10) 之间的,注意右边是开区间,不包括 10 。同样的,p3 中的数据是 a 值在 [10, 20) 之间的,不包括 20 。

也就是说,如果有这么一条数据:

INSERT INTO test_table (`a`, `b`, `c`) VALUES (10,10,20);

由于 a=10,所以会落入 p3 分区。

再来看多列分区,使用第一个多列分区语句,执行 INSERT,会发现,数据插入到了 p2 分区,而不是想象中的 p3 分区。

这里么的原因,就涉及到 MySQL 内部的比较了。当使用单列分区时,MySQL 的比较方法是:

if a < 0  then p1
if a < 10 then p2
if a < 20 then p3

当采用多列分区的时候,比较方法就相应的变成了:

if (a,b,c) < (0 , 0 , MAXVALUE) then p1
if (a,b,c) < (10, 10, MAXVALUE) then p2
if (a,b,c) < (20, 20, MAXVALUE) then p3

那咱们再来看看直接执行这个比较会怎么样:

mysql> SELECT 10 < 10;
+---------+
| 10 < 10 |
+---------+
|       0 |
+---------+
1 row in set (0.01 sec)

mysql> SELECT 9 < 10;
+--------+
| 9 < 10 |
+--------+
|      1 |
+--------+
1 row in set (0.01 sec)

mysql> SELECT (10,10) < (10,10);
+-------------------+
| (10,10) < (10,10) |
+-------------------+
|                 0 |
+-------------------+
1 row in set (0.00 sec)

mysql> SELECT (10,9) < (10,10);
+------------------+
| (10,9) < (10,10) |
+------------------+
|                1 |
+------------------+
1 row in set (0.00 sec)

惊喜来了!(10,10) < (10,10) 毫不意外的被判定为 false ,但是 (10,9) < (10,10) 确是 true 的!

再来一些尝试:

mysql> SELECT (11,9) < (10,10);
+------------------+
| (11,9) < (10,10) |
+------------------+
|                0 |
+------------------+
1 row in set (0.00 sec)

mysql> SELECT (9,11) < (10,10);
+------------------+
| (9,11) < (10,10) |
+------------------+
|                1 |
+------------------+
1 row in set (0.01 sec)

mysql> SELECT (9,10) < (10,10);
+------------------+
| (9,10) < (10,10) |
+------------------+
|                1 |
+------------------+
1 row in set (0.01 sec)

惊呆了,(9,11) < (10,10) 居然也是 true !

来,实际测试一下:

CREATE TABLE `test_table` (
    `a` INT(20) NOT NULL,
    `b` INT(11) NOT NULL
) ENGINE=INNODB DEFAULT CHARSET=UTF8MB4
PARTITION BY RANGE COLUMNS(`a`, `b`) (
    PARTITION p1 VALUES LESS THAN (0, 0),
    PARTITION p2 VALUES LESS THAN (10, 10),
    PARTITION p3 VALUES LESS THAN (20, 20)
);

INSERT INTO `test_table` VALUES (10,10);
INSERT INTO `test_table` VALUES (10,9);
INSERT INTO `test_table` VALUES (9,11);

执行之后发现,第一条记录毫不意外的在 p3 ,但是第二条记录和第三条记录却都在 p2 !

那么这时候执行查询会发生什么呢?

mysql> SELECT * FROM `test_table`;
+----+----+
| a  | b  |
+----+----+
| 10 |  9 |
|  9 | 11 |
| 10 | 10 |
+----+----+
3 rows in set (0.00 sec)

mysql> EXPLAIN PARTITIONS SELECT * FROM `test_table` WHERE a=10;
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
| id   | select_type | table      | partitions | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
|    1 | SIMPLE      | test_table | p2,p3      | ALL  | NULL          | NULL | NULL    | NULL |    3 | Using where |
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

mysql> EXPLAIN PARTITIONS SELECT * FROM `test_table` WHERE b=10;
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
| id   | select_type | table      | partitions | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
|    1 | SIMPLE      | test_table | p1,p2,p3   | ALL  | NULL          | NULL | NULL    | NULL |    5 | Using where |
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

可以看到,当我们使用多列中的第一列做查询时,MySQL 会识别出 p1 分区一定没有数据,所以优化中直接去掉了这个分区,但是搜索了 p2 p3 两个分区。

这也是疑惑点之一,按照 MySQL 的规则,似乎 p2 也一定不会有数据,为啥要搜 p2

再来看下面一个查 b 的语句,会发现根本没有用分区,直接全分区搜索。。。看来 MySQL 也知道可能有一些 b 值并不是存在相应的分区中,需要全表扫描。

具体原因可能需要深入分析 MySQL 源码,这里就先说这么一个需要注意的现象,防止踩坑。。。

nginx rewrite 的一个小坑

今天在配置 Nginx 的时候写了这么一个 location

location /a {
    rewrite /a/(.*) /$1 break;
    ...
}

然后发现当我直接访问 /a 的时候,rewrite 并没有生效,后端收到的还是 /a 而不是我想象中的 / 。想了想可能是结尾 / 的问题,于是这样改:

location /a {
    rewrite /a(.*) $1 break;
    ...
}

结果新的问题来了,由于这样匹配到的 $1 是空的,所以 Nginx 报错了,the rewritten URI has a zero length

所以这种情况下只好这么写:

location /a/ {
    rewrite /a(.*) $1 break;
    ...
}

注意第一行的末尾 / 。这种情况下,访问 /a 会被 Nginx 自动重定向到 /a/ ,然后重写之后的 uri 就是 /,问题解决。

Nginx client_max_body_size 不生效的奇怪问题

最近在配置 Nginx 的 client_max_body_size 的时候遇到了一个非常奇怪的现象,明明配置了这个设置但是却并没有生效。具体配置是这样的:

http {
    client_max_body_size 8k;
    #...
    server {
        location /a {
            proxy_pass http://a.b.c.d:aaaa;
        }
        location /a/upload {
            client_max_body_size 20m;
            proxy_pass http://a.b.c.d:aaaa;
        }
        #...
    }
    #...
    proxy_intercept_errors on;
    error_page 400 404 405 406 /error_page.html;
    error_page 500 501 502 503 504 /error_page.html;
    location /error_page.html {
        internal;
        #...
    }
}

在实际测试中发现,/a/upload 这个接口上传文件还是会失败,而且大小确实是符合限制的。为了确认不是文件大小的问题,这里还尝试了使用了 client_max_body_size 0; 直接关掉限制,可是还是不起作用。查了很多日志都没找到问题,唯一比较奇怪的是,在错误日志中记录的 request method 与实际不符,日志中 $request_method  记录的是 GET,但是客户端上送的确实是标准的 POST,$request  变量打出来的也是 ‘POST /a/upload’。

万般无奈之下只好上 debug 日志,果然找到了问题的根本原因:

[debug] 4593#0: *1685438 http special response: 500, "/a/upload"
[debug] 4593#0: *1685438 internal redirect: "/error_page.html?"

这里可以在 debug 日志中看到,上传接口其实是调用了后端服务的,但是后端返回了 http 500 状态码。这时候,由于 proxy_intercept_errors  和 error_page  的设置,该请求被内部转发到了 /error_page.html  ,但是这个 location 并没有特别配置 client_max_body_size  ,所以使用的是 http 层的设置 8k。而请求中的文件大小显然是大于 8k 的,于是在这里返回了 413 错误。

找到问题之后就很好解决了,只需要在 /error_page.html  这个 location 中也配置 client_max_body_size  就可以了。

这里也体现出了 nginx 一个比较麻烦的问题,就是内部的 error_page 的处理。要记住的一点就是,在不特殊配置的情况下,error_page 也会继承 http 层的各种配置。也就是说,会有很多蛋疼的情况出现:

  1. http 层配置了 ip 白名单限制,但是又不想用默认的 403 页面,于是自定义了 403 的 error_page,发现不生效。原因就是自定义的 error_page 也继承 http 层的白名单限制,导致不在白名单的用户访问这个 403 页面也被拒绝了。解决方法是单独给 403 页面配置 allow all。
  2. 其他的很多参数配置,比如 client_max_body_size 之类会带来 4xx 错误的配置,都有可能由于 error_page 继承 http 层配置,导致奇怪的问题。

Synergy 配置 SSL 失败的解决方法

在激活了 Synergy Pro 之后,会自动生成 SSL 证书并开启 SSL 加密。但是由于某个暂时还未知的 Bug,在 Mac 上第一次自动生成的证书总是不能用的,会报这样的错误:

[2017-01-25T09:57:03] INFO: OpenSSL 1.0.2 22 Jan 2015
[2017-01-25T09:57:18] ERROR: ssl error occurred (system call failure)
[2017-01-25T09:57:18] ERROR: eof violates ssl protocol
[2017-01-25T09:57:18] ERROR: failed to accept secure socket
[2017-01-25T09:57:18] INFO: client connection may not be secure

需要这样解决:

  1. 在 Synergy Pro 的设置中取消勾选使用 SSL
  2. 关闭当前的 Synergy Pro
  3. 打开终端,进入 `~/Library/Synergy/SSL`
  4. 删除目录下的所有文件
  5. 重新打开 Synergy Pro,在设置中勾选使用 SSL,软件会重新生成证书
  6. 停止原先的客户端,重新连接,在弹框中信任证书,问题解决。

如果在客户端连接的时候出现这样的问题:

[2017-01-25T09:59:13] ERROR: ssl error occurred (generic failure)
[2017-01-25T09:59:13] ERROR: error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol
[2017-01-25T09:59:13] ERROR: failed to connect secure socket

说明客户端和服务端有某一方没有启用 SSL。请检查所有服务端和客户端,必须全部启用 SSL 或全部不启用。绝不可以只有某些启用。