一年多以前,我写过一篇文章Java 10 新特性之 AppCDS,文章的最后有一个结论:

虽然 AppCDS 号称可以支持自定义的 Classloader,但是我试了一个 SpringBoot 的应用,发现对于没有在 -classpath 中指定的 JAR 包中的类,并不会有效果。

在 Java 的世界中,自定义的 Classloader 情况太多了,这个大大限制了 AppCDS 的应用,不过,这次看了 JDK13 的 Release Note,很开心看到 JDK13 对 CDS 的功能进行了增强,本次对 CDS 的增强主要是两个方面:

  • 一个是简化了 CDS 的使用,在原来的步骤中,需要生成能够 dump 的 Class 文件的列表,然后再根据这份文件生成 dump 的内容,然后再使用 dump 的内容进行启动的加速。现在一步就可以直接生成 dump 文件了拿过来做启动的加速了,比原来少了一步。
  • 另一个增强是现在 CDS 不仅仅会 dump -cp 指定的类路径下的 Class,并且会 dump 在应用退出之前所有已经加载的类,有个这个特性,CDS 就能够用在各种场景下了。

接下来,我们就尝试一下在 SOFABoot 中使用 Dynamic CDS,首先新建一个 Spring Boot 的应用,并且把 parent 替换为:

1
2
3
4
5
<parent>
    <groupId>com.alipay.sofa</groupId>
    <artifactId>sofaboot-dependencies</artifactId>
    <version>3.1.5</version>
</parent>

然后我们可以编译并且启动一下,看下耗时情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜  time java -jar target/dynamiccds-0.0.1-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.0.RELEASE)

2019-10-08 23:16:55.768  INFO 69815 --- [           main] c.e.dynamiccds.DynamiccdsApplication     : Starting DynamiccdsApplication v0.0.1-SNAPSHOT on lotus.local with PID 69815 (/Users/khotyn/Downloads/dynamiccds/target/dynamiccds-0.0.1-SNAPSHOT.jar started by khotyn in /Users/khotyn/Downloads/dynamiccds)
2019-10-08 23:16:55.772  INFO 69815 --- [           main] c.e.dynamiccds.DynamiccdsApplication     : No active profile set, falling back to default profiles: default
2019-10-08 23:16:56.377  INFO 69815 --- [           main] c.e.dynamiccds.DynamiccdsApplication     : Started DynamiccdsApplication in 0.981 seconds (JVM running for 1.411)
java -jar target/dynamiccds-0.0.1-SNAPSHOT.jar  4.14s user 0.29s system 301% cpu 1.473 total

耗时时间是 4.14s。

然后我们用如下的命令生成一下 Class 的 Dump:

1
java -XX:ArchiveClassesAtExit=dcds.jsa -jar target/dynamiccds-0.0.1-SNAPSHOT.jar

然后我们再执行以下命令就可以使用刚才 Dump 出来的文件了:

1
time java -XX:SharedArchiveFile=dcds.jsa -jar target/dynamiccds-0.0.1-SNAPSHOT.jar

可以看下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜  time java -XX:SharedArchiveFile=dcds.jsa -jar target/dynamiccds-0.0.1-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.0.RELEASE)

2019-10-08 23:20:44.078  INFO 70738 --- [           main] c.e.dynamiccds.DynamiccdsApplication     : Starting DynamiccdsApplication v0.0.1-SNAPSHOT on lotus.local with PID 70738 (/Users/khotyn/Downloads/dynamiccds/target/dynamiccds-0.0.1-SNAPSHOT.jar started by khotyn in /Users/khotyn/Downloads/dynamiccds)
2019-10-08 23:20:44.083  INFO 70738 --- [           main] c.e.dynamiccds.DynamiccdsApplication     : No active profile set, falling back to default profiles: default
2019-10-08 23:20:44.636  INFO 70738 --- [           main] c.e.dynamiccds.DynamiccdsApplication     : Started DynamiccdsApplication in 0.91 seconds (JVM running for 1.462)
java -XX:SharedArchiveFile=dcds.jsa -jar target/dynamiccds-0.0.1-SNAPSHOT.jar  2.94s user 0.30s system 210% cpu 1.540 total

耗时是 2.94s,时间快了将近一秒多,这个时间可能相比于有大量业务逻辑的应用来说意义不大,但是也算是非常可观了。

前段时间工作忙得飞起,一直想着找个时间可以出去放松一下,逃离下钉钉的轰炸。另外从和老婆结婚以来,一直都没有和她一起出国玩过,也想趁着这次机会,弥补一下这个空白。为什么要选择新西兰呢,一方面是因为知道它是魔界的取景地,风景非常优美,另一方面在网上看过皇后镇的照片,看起来就像人间天堂一样,最后也是因为在做攻略的时候发现了100% 纯净新西兰这个网站,了解到新西兰有大量的徒步路线,对于喜欢户外活动的我来说,真的是有非常大的吸引力。

皇后镇

到了新西兰,怎么能不去皇后镇呢,皇后镇除了本身非常美之外,还可以从镇上出发去周边的各种地方玩,往北可以去瓦纳卡,库克山,往南可以去米尔福德峡湾,是在南岛玩非常理想的据点。

如果预算足够的话,可以定一个在瓦卡蒂普湖旁边的酒店,这样醒来之后,拉开窗帘就可以直接看到清澈的瓦卡蒂普湖,再远点儿就是雪山,那是一种非常享受的感觉。

「Copthorne Hotel & Resort Lakefront Queenstown」酒店窗外的景色

在皇后镇有一个非常重要的游玩项目就是天空缆车,缆车本身倒是没有什么特别的地方,和国内各种景区的缆车没有什么差别,但是坐缆车到山顶之后,就可以看到坐落在瓦卡蒂普湖北岸的整个皇后镇,在天气好的时候,整个瓦卡蒂普湖倒影着天空的蓝色,稍远处就是 Double Cone 等雪山,雪山湖景,确实是人间天堂。

天空缆车上去之后拍皇后镇

当然,到了皇后镇,也要享受一下皇后镇的美食,如果你跟我一样是一个要半夜出来觅食的夜猫子的话,那在皇后镇可能就只有「Day & Night」这家便利店可以选择了,大部分的店在晚上十点钟左右就关门了。

我们在皇后镇期间,尝试了几家大众点评上推荐的店,尝试的第一家 FergBurger,刚到皇后镇入住酒店的时候,前台小姐姐就推荐了这家,我们去的时候,果然火爆,排队排了 10 多个人,还有好多人在等着叫号,FergBurger 的汉堡非常大,味道嘛,感觉还凑活(一个汉堡的味道能够好到哪里去,😅),如果两个人吃的话,点一个汉堡就够了,我们点了两个,结果吃不下浪费了。另外尝试的两家店是「Flame Bar & Grill」和「Captains Restaurant」,个人更加喜欢「Captains Restaurant」,要说对于「Flame Bar & Grill」有什么印象,我在写这篇文章的时候已经没有什么印象了,而「Captains Restaurant」我现在还记得他们家的生蚝的那个新鲜劲儿,以及煎三文鱼的那个香味,这两个菜好吃到我们点另一个西冷牛排都有点儿稍显逊色了(还是比「Flame Bar & Grill」的牛排好吃)。

感受下 FergBurger 的汉堡的大小

瓦纳卡

瓦纳卡离皇后镇不远,坐车 2 个小时不到,可以提前在 InnerCity Bus 的网站上买票,瓦纳卡镇得名于瓦纳卡湖,又是一个在湖边的小镇,当然,旁边肯定也有雪山,看起来和皇后镇的风景有点儿类似,但是如果你去罗伊峰徒步的话,想法就会不一样了,罗伊峰徒步也是我们这次去瓦纳卡的主要目标,从瓦纳卡镇到罗伊峰徒步的停车场没有公共交通可以达到,如果你没有租车的话,可以叫出租车过去,25 NZD,价格还是挺贵的。罗伊峰徒步美其名曰徒步,其实就是爬山,而且是不断地往上爬的过程,路上比较单调,到峰顶差不多需要 3 个小时左右,峰顶海拔 1000 多米,山顶上非常冷,需要注意保暖,我们爬到 2 个半小时左右的路程,才知道这里有一个网红打卡点,很多人在这里拍照,的确拍到非常棒的风景,风景比在皇后镇天空缆车上看到的要广阔多了。

罗伊峰徒步的网红打卡点

罗伊峰山顶的柱子

新西兰很多地方都可以玩高空跳伞,瓦纳卡镇旁边也有一个,有一家公司叫做 Wanaka Skydive,可以去小镇上的 iSite 预订,我们是第一次尝试高空跳伞,本来我只想跳一下,不想拍照,因为太贵了,需要 199 NZD,但是老婆非常贴心地帮我买了拍照的服务,后面看起来还是非常值得的,毕竟我可能也就跳这么一次,因此留下照片也显得弥足珍贵。我其实是有一点恐高的,但是奇怪高空跳伞却没有害怕的感觉,我选择的是 12000 英尺的高空跳伞,是整个飞机里面第一个跳的,在还没有反应过来的时候,教练就带着我跳下去了,在自由落地的过程中,感受到的是呼啸而过的风以及从高空看下去的全景,完全没有在掉落的感觉,可能是太高了,等降落伞打开了之后,教练让我尝试了几下操控降落伞,看起来比较简单,左边拉下就是往左转,右边拉就是往右转。如果让我再去体验一次高空跳伞,我可能也不会去了,第一次是无知者无畏,第二次可能就会真的害怕了。

自由落地阶段

降落伞打开阶段

库克山

库克山是新西兰的最高峰,本来我们并没有打算去库克山,而是去米尔福德峡湾,但是因为已经定了皇后镇回奥克兰的机票,去米尔福德峡湾回到皇后镇的话,可能赶不上飞机,就转而选择了库克山一日游,说是库克山一日游,其实大部分时间都是在路上,我们在库克山只待了两个小时,不过仅仅这两个小时,就已经让我喜欢上了库克山,那边有非常多的徒步的路线,包括著名的胡克谷徒步,如果你喜欢徒步的话,请一定要记住在库克山多待上几天,给自己的足够的时间去徒步。遗憾的是我们去库克山那天,胡克谷那边下着小雨,整个库克山也被云雾笼罩着,不过走在山谷之中,也让我真正地感受到了在荒野那种空旷的感觉,感受到了自己的渺小。

荒野中的山

库克山外的山谷,旷野

奥克兰

奥克兰是新西兰最大的城市,是一个天气说变就变的城市,一会儿下雨,马上又是太阳了,而且经常风很大,因此穿一件冲锋衣是非常有必要的,奥克兰是一个在各种火山口上的城市,其中伊甸山就是一个死火山的火山口,就在城市中心。

伊甸山

奥克兰周围的有非常多的小岛,我们去了豪拉基湾的朗依托托岛,要去朗依托托岛可以在 Queens Ferry 购买船票,从 Queens Ferry 到朗伊托托岛差不多 25 分钟的时间,来回船票每个人 36 NZD,朗依托托岛上没有任何商店,并且基本上没有住宿,所以去了朗伊托托岛之前,一定要带上足够的食物以及水,另外一定要注意最后一班回奥克兰的船时间,一般上是下午三点半,我们那天因为潮汐的关系,提前到了两点半。朗伊托托岛是一个火山岛,差不多 600 多年前因为火山喷发而形成,岛上可以看到非常多的火山岩,朗伊托托岛在二战期间也曾经作为一个军事基地,岛上有不少的徒步路线,我们选择了 Coastal Track,中间走错了路,到了废弃的军事遗留基地,很遗憾最后没有去岛上的最高峰。

岸边的火山岩

朗伊托托岛的码头

朗伊托托岛上的海鸟,根本不怕人

遗憾

这次去新西兰一共玩了差不多七天时间,还是有不少的遗憾,一个是没有去成米尔福德峡湾,因为以前定了机票的原因,另一个是在库克山待的时间太短了,除此之外,因为时间的关系,也没有能够去成汤加里罗高山步道。也好也好,留有遗憾,下次再来!

maxresdefault.jpg

从入手闪之轨迹 4 到现在应该有大半年的时间了,平时并没有什么时间可以用来玩游戏,只有周末几个小时的时间,所以通关速度比较慢,我一直以来对于游戏都非常感兴趣,也希望将自己体验过的一些游戏的感受给写下来,在玩游戏方面,我绝对不是一个资深的玩家,更多写的是一些个人的感受,大部分都是一些主观感受,写下来的目的也是为了加强自己的文字的表达能力,让自己有机会可以深入思考游戏的一些体验,而不是通关就算数了。

这次我第一次通关了轨迹系列,首先讲一下整个过程中最大的体感,闪之轨迹 4 是一个对非粉丝不太友好的游戏,游戏刚刚开始的时候,人物一批批地出场,加上大量的世界观设定的名词,如果你之前没有玩过任何轨迹系列的作品,肯定会有和我一样感觉到完全不知道在干些什么,实际上,到了游戏通关的时候,有一些角色之间的关系我都还没有搞懂,如果你是一个非轨迹系列的粉丝,没有玩过轨迹系列的作品,那你得有游戏一开始就有大量的信息涌现到你的面前的准备,只有挺过前面的这一段之后,到悠娜寻找黎恩剧情展开之后,才会稍微有点儿好转。

因为我是以最低难度剧情通关的,所以接下来先来谈一下剧情方面,说实话,闪之轨迹 4 的剧情真的是太过于“中二”,太过于模式化了,敌方阵营的人没有几个人是有坚定的意志的,基本上被主角团打到了之后就开始反省,洗白,一直到倒数第二个 Boss,还在洗白,喂,洗白洗地是不是太过分了,洗到最后 Boss 还没有开始打地时候,就知道打赢了一定会洗白了。另外,剧情其实是挺枯燥,整个剧情总结起来就是找小伙伴,找黎恩,继续找小伙伴,然后开干,特别是第二章和第三章,都在大量地找各种小伙伴,这一段真地是有点儿玩不下去了。

最后还是谈一下战斗吧,其实我应该没有什么资格来谈闪之轨迹 4 的战斗系统,因为我是最低难度通关的嘛,但是无脑输出后面,发现了回合制战斗系统里面一些可以玩的地方,比如有些时候第 N 个回合会有必杀、破防之类的特效,如果刚好这个是一个敌方的回合,你可以通过时间加速把敌方挤到后面去,让这个回合变成己方回合,从而拿到特效,之前在玩 P5 的时候好像并没有这样的设计,这种设计让很少接触回合制游戏的我来说,还是有一点儿新意,感受到了整个游戏一定的可玩性的。

总结一下闪之轨迹 4 的话,如果是 10 分是满分的话,我给打个 7 分吧,如果你没有玩过轨迹系列的话,并且没有好游戏玩了,是可以玩一下的。如果你是轨迹系列的粉丝的话,那就另当别论了,毕竟我并不是粉丝。

本文将从创建一个 SpringBoot 的应用开始,详细讲述如何将一个 SpringBoot 的应用部署到本地的 Kubernetes 集群上面去。

创建 SpringBoot 应用

首先,我们需要创建一个 SpringBoot 的应用,可以到 http://start.spring.io/ 创建一个,在创建的时候,我们需要选择一个 Web 的依赖,以方便部署到 Kubernetes 之后可以看到效果。

创建完成之后,可以修改一下 SpringBoot 应用的 main 函数所在的类,让它成为一个 Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootApplication
@Controller
public class WebDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebDemoApplication.class, args);
    }

    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "Hello, Kubernetes!";
    }
}

这样,在本地启动这个 SpringBoot 的应用之后,如果我们访问 http://localhost:8080/hello 的话,就可以看到 Hello, Kubernetes! 这句话。

创建一个 Dockerfile

为了能够将这个 SpringBoot 的应用部署到 Kubernetes 里面去,我们需要创建一个 Dockerfile,将它打成一个 Docker 镜像:

1
2
3
4
FROM openjdk:8-jdk-alpine
ARG JAR_FILE
ADD ${JAR_FILE} app.jar
ENTRYPOINT [ "java", "-jar", "/app.jar"]

上面的 Dockerfile 是一个非常简单的 Dockerfile,只是将 SpringBoot 应用打包后的 uber-jar 拷贝到容器里面去,然后运行这个 jar 包。有了这个 Dockerfile 之后,我们就可以在本地把 Docker 镜像打包出来了:

1
docker build --build-arg JAR_FILE=./target/web-demo-0.0.1-SNAPSHOT.jar . -t springboot-demo

然后需要注意的是,这样打出来的镜像是在本地的,没有办法被 minikube 找到,所以,要么将这个镜像放到一个中央的镜像仓库上,要么我们使用 minikube 的 docker daemon 来打镜像,这样 minikube 就可以找到这个镜像。

所以,你首先需要在本地将 minikube 安装上去,具体可以看官方的安装教程。安装完成之后,先运行:

1
minikube start

来将 minikube 启动起来,然后可以运行

1
eval $(minikube docker-env)

将 docker daemon 切换成 minikube 的。最后,我们再用上面的 docker build 来进行打包,minikube 就可以看到了。

将应用部署到 minikube 中去

Docker 镜像都准备好了,现在我们可以将应用部署到 minikube 中去了,首先我们需要创建一个 deployment 对象,这个可以用 yml 文件来描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: springboot-demo-deployment
  labels:
    app: springboot-demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: springboot-demo
  template:
    metadata:
      labels:
        app: springboot-demo
    spec:
      containers:
        - name: springboot-demo
          image: springboot-demo
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080

上面的 yaml 文件没有什么特别的地方,除了 imagePullPolicy 需要指定成 IfNotPresent,这样 minikube 才会从本地去找镜像。

有了上面的 yaml 文件之后,我们就可以运行 kubectl apply -f springboot-demo.yml 来让 minikube 将我们的 SpringBoot 的应用的集群给创建出来。

访问 minikube 中的 SpringBoot 集群

现在我们已经将 SpringBoot 应用部署到了 minikube 中去,那么怎么访问这个集群呢,首先我们需要将端口暴露出来:

1
kubectl expose deployment springboot-demo-deployment --type=NodePort

然后运行:

1
minikube service springboot-demo-deployment --url

得到访问的 URL。再在得到的 URL 后面加上 /hello,就可以看到 Hello, Kubernetes! 了。

或者,我们可以直接运行 curl $(minikube service springboot-demo-deployment --url)/hello 来访问。

以上就是如何将一个 SpringBoot 的应用部署到 Kubernetes 里面去的全过程。

Oracle 昨天公布了 Java 10 的 GA 版本,Java 10 里面除了本地变量类型推断之外,还扩展了原来的 CDS 的能力为 AppCDS

什么是 CDS

CDS 的全称是 Class-Data Sharing,CDS 的作用是可以让类可以被预处理放到一个归档文件中,后续 Java 程序启动的时候可以直接带上这个归档文件,这样 JVM 可以直接将这个归档文件映射到内存中,以节约应用启动的时间。

这个特性其实 JDK 1.5 就开始引入了,但是 CDS 只能作用与 Boot Class Loader 加载的类,不能作用于 App Class Loader 或者自定义的 Class Loader 加载的类,其实有点鸡肋,而且这个是 Oracle JDK 的商业特性,在 OpenJDK 中似乎没有。

这次在 Java 10 中,则将 CDS 扩展为 AppCDS,顾名思义,AppCDS 不止能够作用于 Boot Class Loader,App Class Loader 和自定义的 Class Loader 也都能够起作用,大大加大了 CDS 的适用范围。有了 AppCDS,可以给 Java 的应用程序带来两个方面的好处:

  • 可以提升一些大型的 Java 应用的启动速度。
  • 可以提升 Serverless 的应用程序的启动速度。我觉得这个点可能是 Java 10 提供 AppCDS 的主要原因,Serverless 极可能成为未来的应用的一种非常常见的形态,而把 Java 应用在 Serverless 上,相比于其他的语言来说,一个很大的劣势就是 JVM 的启动速度太慢了,虽然像 AWS 的 Lambda,会给 Java 的 Serverless 应用加上 -client 来用 Client 模式跑加快启动速度,但是实际上效果甚微。有了 AppCDS,可以大大加快 Serverless 应用的启动速度,按照 AppCDS 的 JEP 的说明,对于一个 JEdit 来说,AppCDS 可以为 JEdit 提升 20% 到 30% 的启动速度。

尝试 AppCDS

作者写了一个简单的 Java 应用,来测试 AppCDS 的效果。程序的代码已经放到了 Github 上面,大家可以直接去看,这里只给出大概的操作步骤和最后的测试效果。

1. 决定要 Dump 哪些 Class

一般来说,一个 Java 应用程序会包含很多的 Class 文件,但是在运行中,并不是所有的 Class 文件都会被用到,所以,第一步我们需要来决定具体需要使用哪些 Class 文件,你需要给你的运行命令上加上如下的 JVM 参数:

1
-Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=hello.lst

几个参数的意思分别是: * -Xshare:off:这个参数的意思是不做任何内存的共享,也就是不利用 AppCDS 产生的文件来做内存映射。因为是要决定 Dump 哪些类的内存到归档文件中,所以这个参数需要关掉。 * -XX:+UserAppCDS:默认的情况下 AppCDS 不会开启,所以我们需要加上这个参数来开启 AppCDS。 * -XX:DumpLoadedClassList:表示需要把需要做 Dump 的类名写入到哪个文件中。

这个命令执行之后,会出现一个 hello.lst 的文件,里面就是一个个的类名,下面是一部分内容的截图:

2. 将类的内存 Dump 到归档文件中

有一个需要 Dump 的类的列表之后,第二步,我们就可以将类的内存 Dump 到归档文件中了,在这一步中,我们需要将以下的参数加入到 JVM 参数中去:

1
-Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=hello.lst -XX:SharedArchiveFile=hello.jsa

几个参数的含义分别如下:

  • -Xshare:dump:表示现在要进行类的内存的 Dump。
  • -XX:SharedClassListFile:用来指定需要 Dump 的类的列表。
  • -XX:SharedArchiveFile:表示需要将类的内存 Dump 到哪个归档文件中。

运行上面的命令之后,我们会得到一个 hello.jsa 的文件,包含需要的内存信息的 Dump。

3. 使用 Dump 出来的归档文件加快应用启动速度

有了前面的归档文件之后,我们就可以来加速应用的启动速度了,为了使用上述的归档文件,我们需要在 JVM 中加上如下的参数:

1
-Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=hello.jsa

几个参数的含义分别如下:

  • -Xshare:on:表示打开内存映射。
  • -XX:SharedArchiveFile=hello.jsa:表示用来做内存映射的归档文件是 hello.jsa

4. 测试效果

在我的本机上,不使用 AppCDS 和使用 AppCDS 的效果如下:

不使用 AppCDS

使用 AppCDS

可以看到,对于这样一个简单的应用,AppCDS 还是有 20% 左右的启动速度提升的,当然这个应用的很多的启动时间都花在了类加载上,其他的耗时不多,所以效果挺好。如果其他的应用程序的启动时间花在类上的加载时间比较少的话,可能效果就没有这么明显。

看起来 AppCDS 很美好,但是目前我使用下来有几个坑:

  • 虽然 AppCDS 号称可以支持自定义的 ClassLoader,但是我试了一个 SpringBoot 的应用,发现对于没有在 -classpath 中指定的 JAR 包中的类,并不会有效果。
  • 如果你下载 Oracle 的 JDK,需要加上 -XX:+UnlockCommercialFeature 来开启 AppCDS,但是 OpenJDK 却不用,也是很奇怪,😅

这是假期读的第二本书,以下是对本书的阅读的内容的一些总结:

就 Cloud native 这个词来说,已经被市场人员到处在用了,就跟 Microservice 一样,很难清晰地去定义到底什么是 Cloud native。在本书中,作者提到的 Cloud native 其实包含了两个部分的内容,一个是 Cloud native 的基础设施(Cloud native infrastructure),另一个是 Cloud native 的应用(Cloud native application)。作者对于 Cloud native infrastructure 的定义是:

Cloud native infrastructure is infrastructure that is hidden behind useful abstractions, controlled by APIs, managed by software, and has the purpose of running applications. Running infrastructure with these traits gives rise to a new pattern for managing that infrastructure in a scalable, efficient way.

所谓的 Cloud native 的基础设施就是当我们的基础设施(大量的 VM/Docker,复杂的网络,各种各样的储存等等)变的非常复杂和庞大的时候,通过人力是无法高效地进行运维的,在这样的大规模下,我们应该尽量地去避免人工介入,而是需要有一套软件来帮助我们更好地去管理我们的基础设施,可以让运行在这套基础设施上 Cloud native 的应用可以非常方便地被运维,部署,监控等等。那么怎么去设计管理 Cloud native 的基础设施的系统呢,作者提到了几个点,其中包括系统的自举,API 的设计等等,但是我觉得其中最重要的一点是 Reconciler Pattern,关于这个 Pattern,书中提到了四个原则:

  • Use a data structure for all inputs and outputs.
  • Ensure that the data structure is immutable.
  • Keep the resource map simple.
  • Make the actual state match the expected state.

其中最后一点和 Reconciler Pattern 的关系最大,这个也是 Kubernetes 中的方式,我们往 Kubernetes 中去提交一个 Spec,比如要求一个应用的实例的数量应该是 4,那么 Kubernetes 就会尽可能地去保证这个应用的实例的数量是 4,如果一个实例 Crash 了,它就马上新起一个实例。这就是所谓的 Reconciler Pattern,尽量让实际的状态可以和期望的状态匹配上。

Reconciler Pattern 在设计上非常大的一个优势就是它是声明式的,而不是反应式的。用户要做的是按照系统提供出的 API,直接告诉系统你想要什么,比如说,我想要一个应用的实例的数量保持在 4 个,系统就去考虑各种情况,让你的应用的数量保持在 4 个。而反应式的话,则是监听系统的事件,比如你的应用的某个实例 Crash 了,然后你监听到了这个事件之后,就调用系统的 API 去新创建一个实例。显然,对于用户来说,声明式的方式要简单地很多,不容易出错。而反应式则要求每一个应用都去监听系统的事件,不但侵入到了应用,也很容易出错。

在上面的 Reconciler Pattern 的四个原则中,第二点让数据结构是不可变的这一点也是非常重要的,但是我不是很明白这个点和 Reconciler Pattern 具体的关系在哪里?这个原则是说如果我们要修改一个数据结构,我们实际上不是修改它,而是新建一个新的,然后把原来的标记成过期,这样的好处是,我们可以保留数据结构中间的各种版本,当需要看下当前的 Infra 有哪些区别的,就可以把这些版本直接拿出来对比,非常方便地就可以回答线上环境到底发生了什么样的变更。

有了 Cloud native 的基础设施,我们还要有 Cloud native 的应用运行在上面,那么作为基础设施,我们还应该给应用到底提供什么样的能力呢,书中提到了八点:

  • Runtime and isolation(这里的 isolation 是指资源的 isolation,比如 CPU,Memory,Storage 等等)
  • Resource allocation and scheduling
  • Environment isolation(这里的 isolation 指的是环境的 isolation,比如 dev,test,staging,product)
  • Service discovery
  • State management(Readiness, Liveness)
  • Monitoring and logging
  • Metrics aggregation
  • Debugging and tracing

我们可以看到,上面的八个点中其实下面非常多的都是原来传统的中间件在干的一些事情,而在 Cloud native 的基础设施中,这些中间件正在往下沉,直接成为基础设施的一部分,为应用提供能力。而且在 CNCF 中,上面的八个点基本上都有一个对应的产品对应:

  • Runtime and isolation: containers, rkt
  • Resource allocation and scheduling: kubernetes
  • Environment isolation: kubernetes
  • Service discovery: kubernetes
  • State management: kubernetes
  • Monitoring and logging: Prometheus, Fluentd
  • Metrics aggregation: Prometheus
  • Debugging and tracing: OpenTracing, Jaeger

除了上面的八个点之外,我个人认为应该还加上一个点,就是 Network Resilient,这可以极大程度地解决应用之间的网络通信的问题,这个正是 Service Mesh 所提供的能力,在 CNCF 里面对应的产品是 Envoy(奇怪 istio 怎么还没有进入 CNCF)。

总结来说,本书的作者基本上把设计一个 Cloud native infrastructure 所需要做的事情都讲了一遍,包括上面没有提到的测试等等,大部分的内容其实就是 Kubernetes 目前已经做到的一些事情,如果你对 Kubernetes 的设计已经非常清楚了,那么这本书对你的价值可能不大,如果对 Kubernetes 的设计并不熟悉,那么相信你可以从这本书里面学到不少东西。

浅浅出生快半个月了,这半个月的时间基本上没有睡好觉,刚好在豆瓣上看到一个友邻分享了看完这本书的一些看法,挺有意思的,于是花了几个小时的时间看了一下。

本书将的是一位美国妈妈移居法国后,在法国的各种育儿的经历。主要的内容是在法国,爸爸妈妈们是如何培养孩子的,其中不乏拿美国来进行对比(感觉和中国比较类似),法国的育儿理念受到卢梭多尔托的影响,主张孩子是一个独立的个体,即使在非常小的时候,孩子也有自己的认知,有自己的思想的,对待孩子应该是把他作为一个小大人来看待。

作为大人,应该善于倾听孩子们的想法,并且向他们解释这个世界,对他们的信任和尊重会赢得他们对自己的信任和尊重。在培养孩子的时候,应该给他们设定一定的界限,什么时候可以干,什么事情不能干,而在界限之类,孩子可以自由地做任何事情,以睡觉来举例子,可以要求孩子在 9 点钟就进自己的房间,但是具体她在里面做什么样的事情,我们可以不管,这样,孩子在里面玩耍,一会儿累了,就会自己上床睡觉。这样的好处是可以保证孩子的教养,安全,有素质的情况下,最大程度地让孩子的天性可以得到自由的发挥。

当然,除此之外,作为家长,千万不能以孩子为中心,家长应该有自己的空间和时间,也是独立的个体,不能一天到晚地追着小孩子跑,孩子是生活中非常重要的一个部分,但是生活中应该还有其他也非常重要的部分:工作,旅游,夫妻之间的约会等等。不能认为有了孩子,那么家长所有的时间就都是孩子的了。孩子长大了还是会独立,会离开,而你的配偶才是与你度过下半辈子的人。这方面,我们已经见到我们的长辈身上的一些悲剧,我不止听到一个人提起过,他的母亲有说过:「要不是为了你,我早就自杀了」这样的话,这样把孩子作为绝对重心的家庭往往会导致家庭的悲剧,我们这一代人,不能重蹈覆辙。

书中除了写了一些发过的育儿理念之外,也有一些比较实用的操作,比如如何让宝宝养成在几个月内就能够整晚睡觉的习惯,关键就是在孩子半夜哭起来的时候,先等一下仔细观察一下,看下孩子到底是不是真的需要换尿不湿了,或者真的是饿了,可以让她先哭上个几分钟,小孩子的睡眠周期大概是两个小时左右,哭一会,她会尝试控制自己,然后直接进入了下个睡眠周期了。作为家长,在这方面不能太急,需要有点耐心,有点狠(作为一个父亲,我发现女儿哭起来的时候,我就有一种冲过去抱抱她的冲动),控制一下抱她的冲动。这个技巧,我们也在尝试中,期望在浅浅几个月大的时候可以整晚地自己睡眠。

另外一些有用的操作就是如何养成孩子在吃东西上的一些习惯。一个是养成孩子定时吃饭的习惯,方法是让孩子能够等一等,不能孩子想吃的时候就想吃,慢慢习惯之后,孩子会在吃饭的时间尽量吃饱,然后即使饿了,也会自己耐心等一等。另一个是如何给孩子尝试各种食物,就是如果孩子不吃某种食物,可以几天之后再尝试给他吃同一种食物(可以用不同的烹饪方式),经过几次之后,孩子就会吃的。

当然,本书也有不少瑕疵,一个观点抛出来,和这个观点无关的废话有点多。有几个章节,作者主要就是在讲自己的经历(比如,双胞胎出生的那一章节),而没有谈育儿的内容,比较水。总的来讲,书中的一些方法和观点对于新爸爸妈妈们还是有一定的参考意义的,建议有兴趣的可以阅读一下。

虽然今天遇到的不是一个有什么技术含量的问题,但是国内做这块的人太少了,觉得还是需要多分享分享,至少可以帮助新人走一些弯路,^_^

前几天手贱升级了 macOS Sierra,本来看到 Intellij IDEA 在 macOS Sierra 下面只有一个触摸板异常灵活的问题,觉得自己触摸板用地比较少就直接升级了,哪知道升级以后,在开发我们自己的 Intellij IDEA 插件的时候,启动的 IDEA 一会儿就出现了 Crash 的问题(Crash 的是通过 Intellij IDEA 启动起来的用来测试插件的 Intellij IDEA)。具体的 Crash 截图如下:

intellij_idea_crash.png

本来想是不是要直接回到 OS X Yosemite,但是不甘心啊,觉得既然都已经升级了,那遇到问题就解决吧,幸好在 Jetbrains 官方的问题跟踪平台上看到了有了提了类似的问题:https://youtrack.jetbrains.com/oauth?state=%2Fissue%2FJRE-3

看这个帖子的意思是这个问题是 OpenJDK 的 Bug,于是顺藤摸瓜找到了 Jetbrains 在自己维护的 JDK 上对这个问题 Fix 的 Commit:

https://github.com/JetBrains/jdk8u_jdk/commit/02f9a5fbb4924ff67c8a04c15e490acfcc750003

如果把运行插件 SDK 的 JDK 换成 Jetbrains 自己的 JDK,应该就可以解决问题。当然,要使用 Jetbrains 自家的 JDK,不用拿着源代码自己 Build,可以直接从这里下载对应的 Build:https://bintray.com/jetbrains/intellij-jdk

下载过来以后在插件工程的「Project Structure」配置界面进行如下配置即可:

  1. 将下载过来的 JDK 增加到 SDK 里面。
  2. 找到当前正在用来运行插件的插件 SDK,将其以依赖的 JDK 修改成刚刚增加的 JDK。

至此问题就已经解决,在这里建议大家如果要开发 Intellij IDEA 的插件的话,还是用 Jetbrains 自家的 JDK 比较好,毕竟 Jetbrains 已经在 OpenJDK 的基础上 Fix 了不少的问题,特别是很多和 Swing 相关的问题,使用他们的 JDK 可以帮助我们少走不少弯路。


PS:如果有人对开发工具、插件感兴趣,欢迎留言联系我,蚂蚁金服需要最优秀的工程师来做研发工具,提升工程师的效率。

问题

今天在排查一个线上的问题,线上的一个应用在初始化一个类的静态字段的时候出现了 NoClassDefFoundError,并且在导致 NoClassDefFoundError 出现的根本原因消失后,后续再次尝试初始化这个类的时候,持续出现了 NoClassDefFoundError

于是怀疑 JVM 是不是对一个类的 NoClassDefFoundError 做了缓存,在第一次加载这个类出现 NoClassDefFoundError 以后,后续再尝试加载就直接抛出 NoClassDefFoundError

实验

为了证实自己的猜想,尝试设计了一个简单的实验,一个涉及三个类

1
2
3
public class Test1 {
    static Test2 test2 = new Test2();
}
1
2
public class Test2 {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
    public static void main(String... args) throws Exception {
        while(true) {
            System.out.println("================================");
            try {
                new Test1(); // 尝试实例化 Test1,触发 NoClassDefFoundError
            } catch (Throwable e) {
                e.printStackTrace();

                try {
                    Test.class.getClassLoader().loadClass("Test2"); // 尝试加载 Test2,用于证实当将 
                                                                    // Test2.class 拷贝到 ClassPath 下的时候,
                                                                    // Test2 就可以加载到了。
                } catch (Throwable ex) {
                    ex.printStackTrace();
                }
            }
            Thread.sleep(3000);
        }
    }
}

上述类的的作用是:Test2 是一个空的类,Test1 里面有一个 Test2 的静态成员。Test 是程序的主入口,在一个无限循环内部,不断地尝试去实例化 Test1,并且在加载 Test1 出现异常的时候,尝试加载一下 Test2。

实验的步骤是:

  1. 编译以上类,运行 javac Test.java
  2. 将生成出的 Test2.class 重命名成 Test2.class.bak
  3. 运行 java Test,这个时候程序去加载 Test1 的时候,就会出现 NoClassDefFoundError,并且在尝试加载 Test2 的时候,会出现 ClassNotFoundException
  4. 将第二步重命名的 Test2.class.bak 该回成 Test2.class,这个时候程序去加载 Test1 的时候,就会出现 NoClassDefFoundError,在加载 Test2 的时候,不会出现 ClassNotFoundException

实验的第二步的目的是为了程序在加载 Test1 的时候因为找不到 Test2 出现 NoClassDefFoundError,第四步是为了和第二步做对照,说明在后续程序可以加载到 Test2 的时候,在实例化 Test1 的时候,依旧出现 NoClassDefFoundError

在我的机器上,按照上面的方式去操作,结果如下:

NoClassDefFoundError

结果正如预期,即使在后面 Test2 在 ClassPath 下的时候,NoClassDefFoundError 依旧出现,所以 JVM 里面肯定有地方对 NoClassDefFoundError 做了缓存。

JVM 里面的实现

带着这个疑问,请教了部门里面的 JVM 专家,这个猜测得到了证实,并且他给出了 JVM 内部具体处理这段逻辑的代码,处理的代码在 JDK 的 instanceKlass.cpp 这个文件里面:

1
2
3
4
5
6
7
8
9
10
11
12
bool instanceKlass::link_class_impl(
     instanceKlassHandle this_oop, bool throw_verifyerror, TRAPS) {
   // check for error state
   if (this_oop->is_in_error_state()) {
     ResourceMark rm(THREAD);
     THROW_MSG_(vmSymbols::java_lang_NoClassDefFoundError(),
                this_oop->external_name(), false);
   }
   // return if already verified
   if (this_oop->is_linked()) {
     return true;
   }

并且在 instanceClass.hpp 这个文件中,定义了类的 _init_state,其中,is_in_error_state 这个方法的定义如下:

1
bool is_in_error_state() const           { return _init_state == initialization_error; }

^M,神奇的字符!相信很多人写 Shell 脚本的时候都被这个字符坑过,我自己也至少被坑过两次。最近周围的好几个小伙伴又被 ^M 坑,花了好几个小时检查脚本的错误,结果发现是 ^M 导致的。所以写了这篇文章讲一下什么是 ^M,当 ^M 出现的时候一般会伴随着什么样的现象,出现了我们可以用什么手段去解决。

^M 是何方神圣

这个得先从 Windows 和 Unix 下的换行符开始说起,在我的 Intellij IDEA 的右下方的状态栏上,有一块是展示当前文件的换行符的:

Windows 和 Unix 下的文件换行符

可以看到在 Windows 下,换行符是 \r\n,在 Unix 下换行符是 \n。如果我们用把一个文件的换行符换成 Windows 的换行符,那么当我们用 cat -v 来看的时候,就可以看到:

cat -v 查看文件是否含有 ^M

实际上 ^M 就是 Windows 下的换行符中的 \r 部分。因为 Unix 下的换行符是 \n,所以当一个用 Windows 下的换行符的文件放在 Unix 下的时候,单行的最后一个字符就变成了 \r\r 在 ASCII 码中是 0xD,而 0xD 在 VIM 和 cat -v 则刚好被显示为 ^M

刚才之所以用 cat -v 而不用普通的 cat 是因为 ^M 是不可见的字符,如果仅仅用 cat,是看不到这个字符的。cat-v 参数的作用就是显示不可打印的字符。

^M 会导致什么样的问题?

我们已经知道了 ^M 实际上就是 \r,而 \r 是回车符(Carriage Return),回车符的作用是将设备的位置重置到当前行的开头

知道了 \r 的作用时候,我们来看一个现象:

1
2
3
4
5
6
7
### 有一个普通文件,存放了一个路径,当前行的最后以 ^M 结尾
$ cat -v Main
/home/admin/khotyn.huangt/test/^M

### Echo 一下,神奇了!
$ echo "`cat Main`/where am i"
/where am i/khotyn.huangt/test/

看到后面那个 echo 命令,它将 Main 文件中的内容提取出来,再在后面加上 /where am i 这个字符串,结果我们看到,/where am i 在打印的结果中跑到最前面去了,这正是 \r 这个字符的作用,因为 cat Main 的执行的结果的最后一个字符是 \r,所以一遇到这个字符,设备指针就直接回到了当前行的开头,所以 \r 后面的 /where am i 就直接显示在了最前面。

所以,当你看到什么奇怪的路径,这个路径中莫名其妙地少了一些字符,出现了一些莫名其妙的字符串的,很可能就是 ^M 导致的。

如何逃离 ^M 的魔掌

当你发现了 ^M 导致的问题的时候,最直截了当的方式就是将 ^M 从文件中去掉。

一、临时解决的几个方法

如果的机器上安装有 dos2unix,那么恭喜你,直接运行

1
dos2unix /path/to/file

就可以将一个文件的换行符从 Windows 的转换成 Unix 的。

但是,如果机器上没有装 dos2unix,而你又没法装上去(在一家公司工作总是会有各种各样的让你感觉很丧气的权限控制),那么你可以用 sed 来替换:

1
sed --in-place='' 's/^M//g' /path/to/file

注意:上面的 ^M 只是显示的效果,输入的时候需要用组合键输入,先 Ctrl + V,然后马上 Ctrl + M 就可以在终端中输入 ^M 了。

当然,用 tr 之类的命令也可以,不过我一般用 sed 的原因是 sed 加上 --in-place 参数可以做到直接替换原文件,而不用产生临时的文件(危险而高效的操作)。

二、预防此问题

不过,前面说的只是当出现问题的时候如何解决,那么如何预防这个问题呢?

第一个方法当然是直接放大招,换个 Mac 啥的,或者把你的机器上的 Windows 格了,装个 Ubuntu 也好啊。真心觉得 Windows 对于程序员来说真的没有啥好处(我好想听说连微软都开源 .Net 了,并且会提供多平台的支持)。

第二个方法嘛,当在 Windows 下使用各种编辑器的时候,尽量将换行符设置成 Unix 的换行符。不要偷懒用 Windows 的换行符,出现了问题就是好几个小时的排查时间。(目前没有发现有什么场景下有必须用到 Windows 下的换行符的,如果有同学知道有这样的场景的话,不吝赐教)。

三、防止被别人坑

虽然我个人觉得不应该用 Windows,不过还是有同学的确是喜欢用,或者因为不可抗拒的因素而暂时在使用,为了防止出现这个问题,可以在版本管理软件上做控制,比如 Git 就可以设置换行符,当你提交文件的时候,可以将你的所有文本的换行符替换成你设定的换行符,详细可以看 https://help.github.com/articles/dealing-with-line-endings/