下载中文商业版的 WinRAR(非加料版)

众所周知,因为某些蛋疼的原因,现在市面上比较容易下载到的 WinRAR 的中文版都变成了非商用个人版。就算是从 rarlab 或者 其他外国官网通过科学方式,下载到的也是加料版。

具体表现为,安装的时候,许可协议有这么一行字:

安装之后的表现为,每次打开都有广告弹窗,怎么都关不掉。

于是思考了一下,这玩意虽然说个人只给非商业个人版,但是总有人购买的。不可能把这个加料版发给购买用户的。那么就一定有一个地方可以下载未加料的安装包,也就是商业版的安装包。

经过一番查找,果然让我找到了!下载之后安装界面是这样的:

成功!终于不用改 DLL 来解决这个烦人的广告啦~

忘了之前是怎么抓到的这个地址。印象中是在下载别的版本的试用版的时候这个地址被漏出来了,然后通过穷举可以拿到最新的地址。

也有可能是搜索到了相关的下载链接。具体的没印象了。可以看到网上相关链接还是挺多的。

下载地址:

6.21 https://www.win-rar.com/fileadmin/winrar-versions/sc/sc20230223/wrr/winrar-x32-621sc.exe

6.02 https://www.win-rar.com/fileadmin/winrar-versions/sc/sc20210616/wrr/winrar-x64-602sc.exe

6.00 https://www.win-rar.com/fileadmin/winrar-versions/sc/sc20201210/wrr/winrar-x64-600sc.exe

5.91 https://www.win-rar.com/fileadmin/winrar-versions/sc/sc20200708/wrr/winrar-x64-591sc.exe

5.80 https://www.win-rar.com/fileadmin/winrar-versions/sc/sc20191217/wrr/winrar-x64-580sc.exe

5.71 https://www.win-rar.com/fileadmin/winrar-versions/sc20190509/wrr/winrar-x64-571sc.exe

5.70 https://www.win-rar.com/fileadmin/winrar-versions/sc20190304/wrr/winrar-x64-570sc.exe

在腾讯云 TKE 上使用 Helm 的几个小坑

首先,大家都懂,gcr.io 是连不上的,需要使用阿里云镜像或别的镜像

helm init --upgrade -i registry.cn-hangzhou.aliyuncs.com/google_containers/tiller:v2.13.0 --stable-repo-url https://kubernetes.oss-cn-hangzhou.aliyuncs.com/chart --service-account tiller

如果没注意已经部署了,可以先删除:

kubectl delete deployment tiller-deploy --namespace=kube-system

然后,helm install 可能会报没有权限:

Error: release xxx failed: namespaces "default" is forbidden: User "system:serviceaccount:kube-system:default" cannot get namespaces in the namespace "default"

这时候需要新建账户处理权限问题

kubectl create serviceaccount --namespace kube-system tiller
kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller
kubectl patch deploy --namespace kube-system tiller-deploy -p '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}'

注意这里给的权限比较高,高级玩家可以用 yaml 自定义权限,这里不再详细说明。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: tiller
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: tiller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: ServiceAccount
    name: tiller
    namespace: kube-system

然后来说说 Volumes 挂载

这个地方在控制台是没有直接入口的,需要通过修订 YAML 实现。有两种方式,一是通过控制台直接编辑 YAML,二是用 kubectl 自行 patch。注意这里修改的是 deployment 的 YAML

我这里使用 NFS 的方式,需要添加的段是:

spec:
  template:
    spec:
      volumes:
      - name: data
        nfs:
          path: /
          server: 10.0.x.x

最后关于访问方式,在 Service 面板,腾讯云提供了更新访问操作的入口,从这里直接修改就可以。

参考文章:

http://lizhe.name/node/357

https://www.jianshu.com/p/53bbf1f86b8a

https://ezmo.me/2017/09/24/helm-quick-toturial/

减少 Mac – Office365 网络请求和流量

在功能和使计算机保持最新状态这两个方面,Office 2016 for Mac 的默认配置提供了最佳用户体验。

在某些情况下,可能希望阻止应用程序联系网络终结点。

若要防止应用程序发送“使用情况”遥测可从终端手动设置:

defaults write com.microsoft.WordSendAllTelemetryEnabled -bool FALSE
defaults write com.microsoft.WordSendAllTelemetryEnabled -bool FALSE
defaults write com.microsoft.ExcelSendAllTelemetryEnabled -bool FALSE
defaults write com.microsoft.PowerpointSendAllTelemetryEnabled -bool FALSE
defaults write com.microsoft.OutlookSendAllTelemetryEnabled -bool FALSE
defaults write com.microsoft.onenote.mac SendAllTelemetryEnabled -bool FALSE
defaults write com.microsoft.autoupdate2 SendAllTelemetryEnabled -bool FALSE
defaults write com.microsoft.Office365ServiceV2 SendAllTelemetryEnabled -bool FALSE
defaults write com.microsoft.WordSendASmileEnabled -bool FALSE
defaults write com.microsoft.ExcelSendASmileEnabled -bool FALSE
defaults write com.microsoft.PowerpointSendASmileEnabled -bool FALSE
defaults write com.microsoft.OutlookSendASmileEnabled -bool FALSE
defaults write com.microsoft.onenote.mac SendASmileEnabled -bool FALSE
defaults write com.microsoft.errorreportingIsAttachFilesEnabled -bool FALSE

(Copy以上命令行,打开 Mac – 终端,粘贴进去,回车就行了。已包含反馈调查和故障报告)

禁用说明:

遥测

Office 2016 for Mac 会定期将遥测信息发送回 Microsoft。

向“Nexus”终结点上传数据。

遥测数据可帮助工程团队评估每个 Office 应用的运行状况和任何意外行为。

遥测分为两类:

Ø 检测信号包含版本和许可证信息。应用启动时此数据会立即发送。

Ø 使用情况包含应用的使用情况和不严重的错误。此数据每 60 分钟发送一次。

注意:检测信号将始终发送遥测,无法禁用。

 

反馈和调查

使用 Office 2016 for Mac 有可能需要提供 Office 体验反馈。

微软根据反馈内容不断改进产品、了解新功能请求和评估客户对产品改动的满意度。

反馈分为两种类别:用户反馈和调查。

Ø 用户反馈由 Office 使用者发起。他们可以提交评论,或者可选择提供屏幕截图和电子邮件地址。

Ø 调查由 Office 发起,显示为可关闭的通知消息。可以提交评分,同时可选择提交评论。每 3 个月最多请求 1 次调查。

可以在 Office 中基于每个应用程序禁用反馈和调查。

 

故障报告

应用程序出现严重错误时,该应用程序将意外终止并将故障报告上传到“Watson”服务。

故障报告包括一个调用堆栈,它是应用程序所处理并导致故障的步骤列表。

这些步骤可帮助微软工程团队确定失败的确切函数以及原因。

在某些情况下,文档的内容将导致应用程序出现故障。

如果应用将文档标识为原因,它会询问用户是否可以将文档与调用堆栈一起发送。

用户可以在了解信息的情况下对此问题作出选择。

为防止发送文档并禁止向用户显示提示

IPv6 地址处理时的小 Bug

众所周知,IPv6 地址中可以用 ::  来省略一连串的 0,于是,在某些情况下,如果 0 被过度合并了,就会导致地址解析的问题。

最近在 AWS 上启用 IPv6 的时候就遇到了这个情况,AWS 分配给我的地址段是:

2406:da14:5c9::/56

可以看出,AWS 实际给我分配的地址是:

2406:da14:5c9:0000::/56

但是由于连续 0 的省略策略,导致 5c9 之后的真正可以被划分的部分被省略了。于是在 AWS 的系统中,子网划分页面就显示成了这样:

2406:da14:5__::/64

按照正常逻辑应该是这样:

2406:da14:5c9:00__:/56

结果当然就悲剧了。这里我除了填 c9 之外,任何的值都会导致划分出的子网不在上面的地址段内,导致划分失败。

所幸的是 AWS 的系统可以随意的解绑地址段,然后重新分配。于是重新分配之后就好了。

不过这里也是对我们的一个提醒:由于 IPv6 地址书写的特殊性,连续 0 会被缩写,那么在进行地址读取和判断的时候,就一定要先做 ::  的展开,再去做判断哦。

 

记一则由于整数溢出导致的教科书级的死循环

首先铺垫一下,这段代码的输出是什么?

public static void main(String []args){
    System.out.println(Integer.MAX_VALUE + 1);
}

很多人可能很快就能答出来,正确答案是:-2147483648

那么接下来看这段代码:

public static void main(String []args){
    Long total = Long.MAX_VALUE;
    for (int i = 0; i < total; i++) {
        System.out.println(i);
    }
}

乍看之下似乎没啥大毛病,但是结合前面的铺垫,就会发现:

当 i 增长到 Integer.MAX_VALUE  的时候,“奇迹”出现了。接下来,下一个 i 值变为了 -2147483648。跟 total 一比,还是小,于是循环继续。

周而复始,这个循环就永远停不下来了。

当然,这里因为我的简化,还是能比较容易的看出这个死循环的。而实际使用中,这个 total 的取值往往是外部带来的。正常情况下,可能外部取值不会大过Integer.MAX_VALUE ,也就是 2147483648。但是当恶意请求出现或者是正常请求越过边界之后,问题就出现了。

所以当我们定义循环变量的时候,一定要小心。循环变量本身的取值范围一定要大于判断条件。简单来说,在上面的例子中,i 的取值必须要能够大于等于 total 可能的最大值。也就是说,在 total 是 Long 的情况下,i 也必须至少定义为 Long,才不会出现问题。

简单总结一下,就是对于控制循环的变量定义一定要非常谨慎。否则极有可能出现类似很难测出的 Bug,导致各种各样的问题

MyBatis 获取自增 ID 的小坑

在 MyBatis 中,获取自增 ID 主要有两个方法:

  1. 在 SQL 中增加两个属性,useGeneratedKeys  和 keyProperty
  2. 增加 selectKey  语句块,执行自定义 SQL 获取自增 ID

针对这几种方法,有几个小问题需要注意:

  1. useGeneratedKeys  是基于 JDBC 的 Prepared Statement  实现的,具体做法是调用 JDBC 的 getGeneratedKeys  方法,在 Prepared Statement  对象中取相应的值。当 DB 为 MySQL 的时候,会在响应时返回相应的自增字段值。但是,在某些实现 DB 分库分表的 Proxy 中,由于涉及 SQL 转换、重写的问题,可能对 Prepared Statement  的支持并不完整,导致 useGeneratedKeys  选项无法正常返回对应的自增 ID
  2. selectKey  方式是 MyBatis 自动的在执行完 Insert 语句之前/后自动执行对应的语句块,去生成/获取对于的 ID,并填充进相关的字段。在这里要注意,在某些 Prepared Statement  支持不完整的 Proxy 中,需要增加 statementType=”STATEMENT”  来强制指定不使用 Prepared Statement  来获取 ID
  3. keyProperty 这个参数也有一点需要注意。如果在 Mapper.java 中使用了 @Param  来对传入的参数进行了命名的话,这里接收自增值的属性需要用 paramName.fieldName  这种方式来写。如果只写 fieldName 则无法成功回传。最好的方式是不要用 @Param  来传参,而是全部封装成 bean 来进行传递

JVM DNS IP 地址缓存 (InetAddress)

(本文所有内容基于 Oracle JDK)

JVM IP 地址缓存

JVM 的缓存策略

由于 DNS 解析是一个访问量大的不是很可靠的网络调用,因此通常大部分系统都会对 DNS 解析的结果进行一定程度的缓存。如运营商的 LDNS、常用的浏览器、包括操作系统本身,都会对 DNS 解析的结果进行缓存。在 JVM 中,为了加速 DNS 解析的过程,当然也进行了相关的缓存。

在 Java 中,最常用的进行 DNS 解析的方法就是:

java.net.InetAddress.getAllByName(“www.google.com”);

而这个方法本身也会对解析的结果进行相应的缓存。看官方文档:

InetAddress Caching

The InetAddress class has a cache to store successful as well as unsuccessful host name resolutions.

By default, when a security manager is installed, in order to protect against DNS spoofing attacks, the result of positive host name resolutions are cached forever. When a security manager is not installed, the default behavior is to cache entries for a finite (implementation dependent) period of time. The result of unsuccessful host name resolution is cached for a very short period of time (10 seconds) to improve performance.

If the default behavior is not desired, then a Java security property can be set to a different Time-to-live (TTL) value for positive caching. Likewise, a system admin can configure a different negative caching TTL value when needed.

Two Java security properties control the TTL values used for positive and negative host name resolution caching:

networkaddress.cache.ttl
Indicates the caching policy for successful name lookups from the name service. The value is specified as as integer to indicate the number of seconds to cache the successful lookup. The default setting is to cache for an implementation specific period of time.A value of -1 indicates “cache forever”.
networkaddress.cache.negative.ttl (default: 10)
Indicates the caching policy for un-successful name lookups from the name service. The value is specified as as integer to indicate the number of seconds to cache the failure for un-successful lookups.A value of 0 indicates “never cache”. A value of -1 indicates “cache forever”.

简单来说,在默认情况下,成功解析到 IP 的解析结果会被永久缓存,而解析失败的结果会被缓存 10s。

虽然在一般情况下,这个缓存有利于提高系统的效率,减少网络交互。但是当我们依赖 DNS 进行负载均衡的时候,就会出现问题了。

修改策略

想要修改 JVM 默认的缓存策略,有三种方法实现:

  1. 修改 java.sercurity 配置文件
  2. JVM 启动时添加启动参数
  3. JVM 启动后,通过 System 修改系统类属性

修改配置文件

在 JDK 的 %JAVA_HOME%/jre/lib/security  目录下存在 java.security 文件。通过修改文件中的 networkaddress.cache.ttl  和 networkaddress.cache.negative.ttl  可以达到修改缓存策略的目的。配置信息如下:

#
# The Java-level namelookup cache policy for successful lookups:
#
# any negative value: caching forever
# any positive value: the number of seconds to cache an address for
# zero: do not cache
#
# default value is forever (FOREVER). For security reasons, this
# caching is made forever when a security manager is set. When a security
# manager is not set, the default behavior in this implementation
# is to cache for 30 seconds.
#
# NOTE: setting this to anything other than the default value can have
#       serious security implications. Do not set it unless
#       you are sure you are not exposed to DNS spoofing attack.
#
#networkaddress.cache.ttl=-1

# The Java-level namelookup cache policy for failed lookups:
#
# any negative value: cache forever
# any positive value: the number of seconds to cache negative lookup results
# zero: do not cache
#
# In some Microsoft Windows networking environments that employ
# the WINS name service in addition to DNS, name service lookups
# that fail may take a noticeably long time to return (approx. 5 seconds).
# For this reason the default caching policy is to maintain these
# results for 10 seconds.
#
#
networkaddress.cache.negative.ttl=10

JVM 启动时修改启动参数

同样的可以通过启动参数的方式来改变这个值:

https://docs.oracle.com/javase/8/docs/technotes/guides/net/properties.html

sun.net.inetaddr.ttlThis is a Oracle JDK private system property which corresponds to networkaddress.cache.ttl. It takes the same value and has the same meaning, but can be set as a command-line option. However, the preferred way is to use the security property mentioned above.


sun.net.inetaddr.negative.ttlThis is a Oracle JDK private system property which corresponds to networkaddress.cache.negative.ttl. It takes the same value and has the same meaning, but can be set as a command-line option. However, the preferred way is to use the security property mentioned above.

运行时通过 System 类修改

通过 java.lang.System  类在JVM启动后修改。

System.setProperty("sun.net.inetaddr.ttl", "60");
System.setProperty("sun.net.inetaddr.negative.ttl", "10");

或者

java.security.Security.setProperty("networkaddress.cache.ttl", "60");
java.security.Security.setProperty("networkaddress.cache.negative.ttl", "10");

 

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++ 中不会遇到的问题。