普通视图

发现新文章,点击刷新页面。
昨天以前旅行者的随想

Next SSR 开发与实践

前言

五一假期期间,我发布了 PicImpact 的第一个公开版本,是 Kamera 的下一代产品,使用 Next.js 开发,而 kamera 是用的 Nuxt3 开发的。前端的 SSR 框架,并不像后端的框架那么好用,后端的各种中间件、开发模型、面向切面编程(AOP)、统一异常处理等等,这些可以高度抽象且降低重复劳动的东西,基本上是别想太多。

Next.js 从 v12 开始 Beta 中间件,到我用的 v14 版本,已经趋于稳定了。但是它有一个最致命的限制:Middleware only supports the Edge runtime. The Node.js runtime cannot be used.在这一点上,Nuxt3 的中间件,是要比 Next 好用许多的。但 Nuxt3 也有很多不好的地方,有太多的官方模块,迟迟没有推出。就比如我在实现用户登录的时候,要研究写入 cookies 的最佳实践,当然我最后还是选择了用 pinia 来手动管理。

我用 Nuxt3 和 Next.js 写了一些开源和 private 项目,就目前给我的感觉而言,双方的路线还是有很大的差异化的。没有说谁最好,但是选型时还是可以根据需求和对应框架提供的能力来决定,况且 Vue 和 React 生态都有我喜欢但另一边没什么平替的库。

这篇博客,基于我开发过的一些项目(PicImpact),来梳理大概的流程和实践,不管你有没有实际开发经验,当你想上手时,都会帮你绕过一些坑。

概述

阅读前其实希望读者对 React 和 JavaScript 有基本的了解,组件、道具、状态和钩子之类的。文章可能会涉及到下面这些知识:

  • 路由:布局、页面、路由跳转、中间件等等,都跟路由相关。
  • 数据获取和重新验证:在 server 和 client 如何获取数据。
  • 状态机制:在改变数据的同时,制作最佳的用户体验。
  • 错误处理:一点小经验。
  • 身份验证:处理登录和认证,并结合中间件保护路由和“接口”。
  • 数据迁移:基于 prisma 迁移表结构,Next.js 控制数据更新生命周期。
  • 无障碍:虽然很难在好的设计和无障碍之间平衡,但也并非无法改善。
  • Edge Runtime:最怕 Only Edge Runtime,谁懂啊!

实践

实践部分,主要基于 PicImpact 来写。

布局、页面和路由

我们可以看到,首页的布局,采用了 2 大块布局,在 Header 部分,会在服务端渲染路由部分,点击不同的路由,跳转到不同的页面。在这里只有第一次是服务端渲染,后续的路由渲染会变为客户端渲染,这是因为 useRouteruseParams 都是客户端组件,要在客户端进行 active 的判断,而初次服务端渲染,可以保证首屏体验最佳。

在布局的设计上,我采用了 Route Groups,用来区分不同的业务布局,而不是单纯的父子嵌套关系。这点在 Nuxt 那边的体验就好点,Next 会更加考验你对业务的抽象能力。

数据获取

获取形式主要分两大类:API(通过 api 请求 server 或者第三方服务)、数据库查询。

API 是应用程序代码和数据库之间的中间层,一般在客户端请求 API 层,然后获取数据,避免直接把数据库暴露给客户端。这在前后端分离的场景中很常见,前端请求接口,后端提供业务逻辑和数据库交互能力。

注:Next.js 中,常把 API 叫做 Route。在后端我们常说的接口,在 Next 中就是一个特殊的 route,是一个在服务器上运行的 API 层,它可以处理 req 和 res。

React Server Components

在 Next.js 中,我们也可以用“服务器操作”的形式来获取数据,也就是常说的 RSC。

1
2
3
4
5
6
7
8
export default async function Page() {
const getData = async () => {
'use server'
return await fetchDatabase()
}
const data = await getData()
return <main>{data}</main>
}

这样可以不用写“接口”也能直接查询数据库,并且不会暴露到客户端。

SWR

讲到这里,就不得不介绍 SWR 了,一种由 HTTP RFC 5861 推广的 HTTP 缓存失效策略。这种策略首先从缓存中返回数据(过期的),同时发送 fetch 请求(重新验证),最后得到最新数据。

先来看一个最简单的原生的 fetch 请求:

1
2
3
4
5
async function getData() {
const data = await fetch('https://baidu.com')
.then((res: any) => res.json())
console.log(data)
}

在真实的业务场景,我们必须得知道每个异步请求的状态,对吧?因为要根据状态显示不同的 UI,比如在请求时,页面上显示 Loading... ,结束请求后正常渲染结果。用专业点的话说就是,每一个异步请求,都是一个独立的状态机,每个都会在不同的状态中流转:

  • data:有响应数据,代表请求成功。
  • error:有错误,代表请求失败。
  • isLoading:请求是否正在进行。

在 SWR 中,有一个 isValidating 状态:

  • 无论数据是否已加载,只要有一个正在进行中的请求,isValidating 都会变为 true
  • 当数据尚未加载并且有一个正在进行的请求时,isLoading 会变为 true

看到区别了吗?没错,实际场景中,isLoading 通常放在第一次渲染用,而 isValidating 适用于已经有数据时的自动更新/静默更新场景。

我开发时,由于业务并没有分这么细,所以偷懒直接 isLoading && isValidating 了。

你看哈,如果每一个异步请求都要维护状态,是不是很麻烦?所以 SWR 就很好的解决了我们的痛点,并且它还有其它更好用的 api。

1
const { data, error, isLoading, isValidating, mutate } = useSWR(key, fetcher, options)
  • key: 请求的唯一 key string(或者是 function / array / null)
  • fetcher:(可选)一个请求数据的 Promise 返回函数
  • options:(可选)SWR hook 的配置选项对象
1
2
const { data, error, isLoading, isValidating, mutate } = useSWR('https://baidu.com',
(url: string) => fetch(url).then((res) => res.json()));

我们用 useSWR 改造之前的 fetch 请求,可以看到,url 直接用作了状态机的 key,同时也可以作为参数传递给 fetcherfetcher 也正是请求函数。

重新请求

为啥我上面把 key 叫做状态机的 key 呢?因为咱们可以在客户端的任何组件中,获取全局配置,来 mutate 触发状态机的状态,重新获取数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { useSWRConfig } from 'swr'
export default function Page() {
const { mutate } = useSWRConfig()
return (
<Button
onClick={async() => await mutate()}
>
更新
</Button>
)
}

这是我觉得最好用的功能之一,同时解决了代码复用和组件 props 传递的烦恼。同时我们也可以利用自动重新请求机制来获得更好的用户体验,这就区别于 mutation 的手动控制了:

1
2
3
const { data, error, isLoading, isValidating, mutate } = useSWR('https://baidu.com',
(url: string) => fetch(url).then((res) => res.json())
, { refreshInterval: 1000 });

这里的 refreshInterval 用于控制定期重新请求。

状态机制

图中的模式,表示在开启 keepPreviousData 选项并设置预设数据时,请求数据,再改变 key 值,并在之后进行重新请求的场景。

可以看到,我们改变了值并重新加载,也可以保留之前的数据。并且在分页场景下,点到下一页时,仍旧会保留上一页的数据,这样避免了数据的重复获取,也提升了用户体验。

错误处理

文档中有推荐用 error.tsx 来捕获意外错误,并显示错误页面给用户,但我不是很喜欢这种方式(我写的错误页面不好看...),所以我常用下面的方式尽可能处理错误:

1
2
3
4
5
6
const { data, error, isLoading, isValidating, mutate } = useSWR('https://baidu.com',
(url: string) => fetch(url).then((res) => res.json()));
if (error) {
// 处理错误
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
export async function Page() {
try {
const findAll = await db.$queryRaw`
 SELECT
 *
 FROM
 "public"."Images"
 `
} catch (error) {
throw new Error('查询错误');
}
}

身份验证

身份验证框架,我选择了 Auth.js,它抽象化了管理会话、登录和注销以及身份验证其他方面所涉及的大部分复杂性。挺省事儿的,唯一不爽的地方,就是 middleware 在 vercel 上只能在 Edge 运行时跑,真的烦。

数据迁移

用 prisma 来迁移表结构就不多说了,这里主要介绍 Next.js 的 Instrumentation,可以在启动新的 Next.js 服务器实例时,将调用该函数一次。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import { PrismaClient } from '@prisma/client'
export async function register() {
try {
if (process.env.NEXT_RUNTIME === 'edge') {
return
}
const prisma = new PrismaClient()
if (prisma) {
await prisma.$transaction(async (tx) => {
// ...
 })
console.log('初始化完毕!')
await prisma.$disconnect()
} else {
console.error('数据库初始化失败,请检查您的连接信息!')
}
} catch (e) {
console.error('初始化数据失败,您可能需要准备干净的数据表,请联系管理员!', e)
}
}

无障碍

可访问性(Accessibility)是指设计和实现每个人(包括残障人士)都可以使用的 Web 应用程序。这是一个涵盖许多领域的广阔主题,例如键盘导航、语义 HTML、图像、颜色、视频等。

在开发时,我们可以用 next lint 命令来检查项目中的可访问性问题,在 Next.js 中包含了一个 eslint-plugin-jsx-a11y 插件。同时记得添加 aria-label 标签。

最后

得益于 React 和 Next.js 的强大生态,以及之前进行 SSR 开发的经验,这次在设计 PicImpact 时,算是少走了不少弯路,它有着更好的设计、更优的性能,我在保留之前大部分功能的同时,进行了更多良性的可拓展设计。

当然这个项目也还有很多不足的地方,Next.js 本身也是一个很考验代码抽象能力的框架,更何况还用了 typescript。

最后感谢你能看到这里,有想法欢迎与我交流!

基于 K3s + Kilo 的跨云 Kubernetes 集群

前言

自从白嫖了 Oracle Cloud 的云服务之后,一直使用的 24 G 大内存云服务器跑的 Kubernetes,也有 2 年多了吧?(记不清了)上一版本的方案,可以看我这篇文章👉Dynamic k8s 集群实现方案

至于为啥我要更换方式呢?主要还是要部署的东西日渐增加,内存是越来越不够了。原先的方案是相当于备份,假设当前使用的集群挂了,那么直接指向另一个集群就能马上恢复服务,那么服务不可用的时间,主要还是 DNS 解析变更时间。(这里放上之前的图片,便于理解)

今天要聊的新的架构,则是下图所示:

虽然依旧不用担心数据丢失问题(数据库和部分存储全部在集群外部),但是相比原来的可用性方面略有下滑,毕竟集群从原来的多个,变成了现在的一个。但好处是获得了更大内存的集群,这样就可以充分利用原来因为资源不够而不太好上的一些功能,比如蓝绿部署、金丝雀发布,以及用来模拟正式环境的流量镜像(Traffic Mirroring)功能,这样我在本地开发时,也有足够的内存来通过 kt-connect 在集群内调试服务了。

而这次我选择了更轻量级的 K8S 版本——K3s,也是想节约一下资源。这样也正好能接入更多我在其它云厂商的大大小小的服务器了,官方的要求是最低 1 核 512 MB 内存。当然,我们也可以部署高可用 K3s,本文为了方便演示,就用一个控制平面节点,两个工作节点来演示。

集群部署

期待的架构模样已经描述好了,接下来我们开始实践部署!

部署前准备

机器环境如下,准备了 3 台相同配置,但分别在 3 个不同国家/地区的云服务器,且无法通过内网连接,只有公网 IP:

OS: Debian GNU/Linux 11 (bullseye) aarch64
Host: KVM Virtual Machine virt-4.2
Kernel: 5.10.0-26-arm64
Shell: bash 5.1.4
CPU: (4)
GPU: 00:01.0 Red Hat, Inc. Virtio GPU
Memory: 24003MiB

首先,需要更改每一台主机名称:

1
hostnamectl set-hostname xxx

3 台主机更改完毕后,名称为:

kube-japan // 主节点
kube-seoul
kube-chuncheon

其次,我们需要在每台机器上安装 wireguard:

1
apt-get install wireguard -y

主节点部署

首先我们需要在主节点上安装一个功能齐全的 Kubernetes 集群,它包括了托管工作负载 pod 所需的所有数据存储、control plane、kubelet 和容器运行时组件。

1
2
3
4
5
6
7
8
9
curl -sfL https://get.k3s.io | K3S_CLUSTER_INIT=true \
INSTALL_K3S_EXEC="server" \
INSTALL_K3S_CHANNEL=v1.27.8+k3s2 \
sh -s - --node-label "topology.kubernetes.io/region=japan" \
--node-label "master-node=true" \
--tls-san <主节点公网IP> \
--advertise-address <主节点公网IP> \
--flannel-backend none \
--kube-proxy-arg "metrics-bind-address=0.0.0.0"

kubeconfig 文件会被写入到 /etc/rancher/k3s/k3s.yaml,由 K3s 安装的 kubectl 将自动使用该文件。

K3S_CLUSTER_INIT=true 将安装为集群模式。

INSTALL_K3S_EXEC="server" 表示安装为 Server 节点。

INSTALL_K3S_CHANNEL=v1.27.8+k3s2 表示安装的版本。

--node-label "topology.kubernetes.io/region=japan" 设置节点的 label。

--node-label "master-node=true" 设置为主节点。

--tls-san--advertise-address 要填主节点的公网 IP。

--flannel-backend none 因为我们等会儿要用 Kilo,所以这里要关闭 fiannel CNI。

详细请参考环境变量

  • 查看节点状态:
1
kubectl get node

节点是 NotReady 状态,别急,我们安装了 Kilo 就行了。

install Kilo!

  • 首先定义主节点的网络拓扑:
1
2
3
kubectl annotate nodes kube-japan kilo.squat.ai/location="japan"
kubectl annotate nodes kube-japan kilo.squat.ai/force-endpoint="<公网IP>:51820"
kubectl annotate nodes kube-japan kilo.squat.ai/persistent-keepalive=20

location 定义节点的位置,Kilo 将尝试从 topology.kubernetes.io/region 节点标签推断每个节点的位置。

force-endpoint 定义节点的端点,因为咱们的服务器位置位于不同的云提供商或不同的专用网络中,则端点的 host 部分应该是可公开访问的 IP 地址或解析为公共 IP 的 DNS 名称,以便其它位置可以将数据包路由到它。

persistent-keepalive 持久保活注释参数配置,想了解可以看 WireGuard 文档

  • 然后安装 Kilo:
kubectl apply -f https://raw.githubusercontent.com/squat/kilo/main/manifests/crds.yaml
wget https://raw.githubusercontent.com/squat/kilo/main/manifests/kilo-k3s.yaml

注意 kilo-k3s.yaml 文件,咱们需要添加以后参数,这里我们采用的是 Full Mesh 模式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
- name: kilo
 image: squat/kilo
 args:
 - --kubeconfig=/etc/kubernetes/kubeconfig
 - --hostname=$(NODE_NAME)
 - --mesh-granularity=full  # 添加这一行
 env:
 - name: NODE_NAME
 valueFrom:
 fieldRef:
 fieldPath: spec.nodeName
  • 然后再执行:
1
kubectl apply -f kilo-k3s.yaml

然后咱们应该可以看到,节点状态变为 Ready 了。

部署 K3s Agent 节点

首先咱们现在 Server 节点上获取 server token

cat /var/lib/rancher/k3s/server/node-token
  • 同样的,在 node 节点上执行命令:
1
2
3
4
5
6
7
curl -sfL https://get.k3s.io | K3S_URL=https://<主节点公网IP>:6443 \
K3S_TOKEN=<主节点 server token> \
INSTALL_K3S_EXEC="agent" \
INSTALL_K3S_CHANNEL=v1.27.8+k3s2 \
sh -s - --node-label "topology.kubernetes.io/region=chuncheon" \
--node-label "worker-node=true" \
--kube-proxy-arg "metrics-bind-address=0.0.0.0"

这里的参数不需要过多解释了,可以往上翻翻。

  • 然后在主节点查看是否加入进来:
1
kubectl get node
  • 在主节点上添加网络拓扑定义:
1
2
kubectl annotate nodes kube-chuncheon kilo.squat.ai/location="chuncheon"
kubectl annotate nodes kube-chuncheon kilo.squat.ai/persistent-keepalive=20

另一台也是同样的操作,注意参数的值略有不同!!!

至此,我们就安装配置完整个集群了,然后我们查看整个集群的网络拓扑图:

1
kgctl graph | circo -Tsvg > cluster.svg

集群面板部署

现在咱们已经搞定了集群了,再来部署集群的 Web 面板吧,这里我选了 UI 好看点的 KubeSphere

安装 KubeSphere

  • 执行安装命令:
1
2
3
kubectl apply -f https://github.com/kubesphere/ks-installer/releases/download/v3.4.1/kubesphere-installer.yaml
kubectl apply -f https://github.com/kubesphere/ks-installer/releases/download/v3.4.1/cluster-configuration.yaml
  • 查看安装日志:
1
kubectl logs -n kubesphere-system $(kubectl get pod -n kubesphere-system -l 'app in (ks-install, ks-installer)' -o jsonpath='{.items[0].metadata.name}') -f

安装完毕后,访问使用 IP 端口的方式访问面板,可以看到集群节点状态:

集群公网访问

既然咱们集群配置好了,自然是把面板配置成域名访问最好不过了。这里就要借助 Kilo 和 Cloudflare Tunnel 来完成了,对于 Ingress-Nginx Controller 的控制,我们采用 cloudflare-tunnel-ingress-controller 来实现。

你至少得会用 Cloudflare 吧,既然都会玩 K8S 了,这对你来说应该不成问题。

安装

  • 首先添加 Helm 仓库:
1
2
helm repo add strrl.dev https://helm.strrl.dev
helm repo update

也可以直接用 KubeSphere 操作,见下图:

  • 然后安装:
1
2
3
4
5
helm upgrade --install --wait \
 -n cloudflare-tunnel-ingress-controller --create-namespace \
 cloudflare-tunnel-ingress-controller \
 strrl.dev/cloudflare-tunnel-ingress-controller \
 --set=cloudflare.apiToken="<cloudflare-api-token>",cloudflare.accountId="<cloudflare-account-id>",cloudflare.tunnelName="<your-favorite-tunnel-name>"
  • cloudflare-api-token 你得自己去 Cloudflare 配置令牌,记得要下面三个权限:

    • Zone:Zone:Read

    • Zone:DNS:Edit

    • Account:Cloudflare Tunnel:Edit

  • cloudflare-account-id 你用的那个域名的 账户 ID,也是在 Cloudflare 控制台获取。

  • your-favorite-tunnel-name 这个是通道的名称。

安装完成后,你应该能看到下面的 Pod:

配置应用路由

接下来,咱们只需要创建对应的 Ingress,将面板通过 Cloudflare-tunnel 公开到互联网就好了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
 name: dashboard-via-cf-tunnel
 namespace: kubesphere-system
 finalizers:
 - strrl.dev/cloudflare-tunnel-ingress-controller-controlled
spec:
 ingressClassName: cloudflare-tunnel
 rules:
 - host: example.com # 你要公开的域名
 http:
 paths:
 - path: /*
 pathType: Prefix
 backend:
 service:
 name: ks-console # 指定的 Service 名称
 port:
 number: 80 # 指定的端口

如果你懒得创建 yaml 来执行,也可以:

1
2
3
4
kubectl -n kubesphere-system \
 create ingress dashboard-via-cf-tunnel \
 --rule="example.com/*=ks-console:80"\
 --class cloudflare-tunnel

然后我们就可以在 Cloudflare 的控制台看到对应的隧道里面多了一条 Public Hostname 数据,说明已经成功了,现在可以直接通过域名访问了。

通过 Cloudflare-tunnel 来访问网站,可以搭配 Zero Trust 来实现网关配置(DNS、防火墙策略、流量出口策略等),配置身份提供商组、IP、设备、证书或服务令牌访问,以及监控等功能。

最后

虽然这套方案可以通过一系列措施来保证高可用性,但我还是决定将数据存储配置成外置存储,不放在集群内部。这样一旦集群遇到问题,我就可以通过 YAML 文件在短时间内恢复,前提是数据库部分也得可靠。

参考资料:

嗯学英语核心业务设计实践

前言

最近在设计开发嗯学英语项目,今天想简单讲点儿故事,一个关于英语学习软件背后复杂的技术设计思考,以及我打算如何实践,让它以何种方式运作。

摸索

英语类学习软件我用过不少,使用时间最长的,那自然是多邻国了。你看你学了这么久英语了哈,如果现在要你来做一款英语学习软件,你要如何设计(doge

功能梳理

一句话总结,就是查询单词信息,记录用户学习状态。看着很简单对吧?相信我,如果有人这么跟你提需求让你实现,还跟你说很简单,你一定想拍死他。

开个玩笑哈哈,现在我们来详细分析一下业务:

  • 首先我们得让用户选择学习哪一个词库的,我们就拿《考研英语词汇》来说吧,一共 4533 个词。

  • 在用户选择词库后,我们可以生成一条该用户对这个词库的学习汇总数据,也就是哪些单词用户“学习”了,总共学了多少个等等。

  • 然后用户在软件上获取单词进行学习,在学习完成后,数据将会进行入库操作。这里的入库操作就涉及到比较多的点了,比如咱们要记录单词用户是否“学过”,也就是说不管题目是答对还是答错,都算学过。然后就是单词的进度统计,总共多少个,学了多少个了。以及把用户做错的也记录下来,形成错题本。

这里有必要给大家解释一下,为什么会需要记录学习数据。相信很多小伙伴应该都听过艾宾浩斯遗忘曲线,而我们让用户学习,也不可能一个请求就给一个单词,那样不仅效率低,而且服务器也会绷不住。正确的做法是,生成一个 List,相信用 Excel 背过单词的小伙伴是会深有体会的。因为某个单词用户学习了,入库肯定是会做时间记录的,结合用户的行为,以及错题本,就可以根据一些“记忆算法”来生成 List。由于这个功能只是整块业务的一小部分,这里就不作展开了。

通过这个开发中的初版 UI 草图,我们可以理解一下,以上的功能映射到用户端的大致模样。

数据库表设计

梳理完业务功能后,首先我们要设计数据库表。在消耗了几杯咖啡,挠掉了一些头发后,我设计了如下的数据库表结构(当然,开发迭代的过程中也是会做出调整的):

  • 用户活动词库表,用来记录用户当前正在学习的词库、学过的词库,以及统计已学单词数量。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
CREATE TABLE `enstudy_user_book_dict` (
 `id` bigint NOT NULL AUTO_INCREMENT,
 `user_id` bigint NOT NULL COMMENT '用户 id',
 `book_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '词典id',
 `book_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '词典名称',
 `studied` int NULL DEFAULT NULL COMMENT '已学词数',
 `action` tinyint NOT NULL DEFAULT 0 COMMENT '用户使用状态:0->停用状态;1->使用状态',
 `creator` bigint NULL DEFAULT NULL COMMENT '创建者',
 `updater` bigint NULL DEFAULT NULL COMMENT '更新者',
 `create_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
 `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
 `del` tinyint NOT NULL DEFAULT 1 COMMENT '逻辑删除:0->删除状态;1->可用状态',
 PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户活动词库表' ROW_FORMAT = Dynamic;
  • 用户学习行为表,用来记录用户“学习”过的单词,以及学习时间等。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
CREATE TABLE `enstudy_user_work_actions` (
 `id` bigint NOT NULL AUTO_INCREMENT,
 `word_id` bigint NOT NULL COMMENT '单词id',
 `user_id` bigint NOT NULL COMMENT '用户 id',
 `book_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '词典 id',
 `state` tinyint NULL DEFAULT 0 COMMENT '学习状态:0->未学;1->已学',
 `creator` bigint NULL DEFAULT NULL COMMENT '创建者',
 `updater` bigint NULL DEFAULT NULL COMMENT '更新者',
 `create_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
 `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
 `del` tinyint NOT NULL DEFAULT 1 COMMENT '逻辑删除:0->删除状态;1->可用状态',
 PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户学习行为表' ROW_FORMAT = Dynamic;
  • 用户错题本,用来记录用户学习错误的单词,以及次数。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
CREATE TABLE `enstudy_user_wrong_word` (
 `id` bigint NOT NULL AUTO_INCREMENT,
 `word_id` bigint NOT NULL COMMENT '单词id',
 `user_id` bigint NOT NULL COMMENT '用户 id',
 `book_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '词典 id',
 `fail_count` int NULL DEFAULT 0 COMMENT '错误次数',
 `creator` bigint NULL DEFAULT NULL COMMENT '创建者',
 `updater` bigint NULL DEFAULT NULL COMMENT '更新者',
 `create_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
 `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
 `del` tinyint NOT NULL DEFAULT 1 COMMENT '逻辑删除:0->删除状态;1->可用状态',
 PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户错题本' ROW_FORMAT = Dynamic;

流程

现在咱们来看看,整套流程是怎么走的:

就是一个很典型的 Spring Boot 单体架构服务,对吧?用户端操作,请求到后端服务,然后后端服务操作数据库,并返回。有一点需要注意,服务本身是“无状态”的,所有的状态咱们都在数据库和分布式缓存中进行维护,否则的话就没法舒适地扩展了。

这里的“无状态”指的是,服务端的无状态认证(Authentication),属于“会话状态”的讨论范畴,不要弄混淆了。因为我把用户的状态写入了分布式内存中,而不是当前服务的缓存中,所以不会因为扩展了多个实例而导致状态隔离。

那么到这里为止,咱们就把核心业务给设计完了。

改进

服务器瓶颈改进

由于嗯学英语并不是离线客户端模式,所以还是很依赖于云服务的。我们不可能说,开发一个软件,就只能让一两个人用,人一多就崩溃对吧?那么我们这里做个假设,我们的用户跟多邻国一样多(可以压测模拟),这时候应该如何调整架构来支持呢?

首先说一下目前嗯学英语的整个技术栈和架构,然后我们以此为基础代入思考。

前端:Nuxt 3、Vue 3、TypeScript、Naive UI、TailwindCSS

后端:Spring Boot 3、OpenJDK 17

数据库及中间件:MySQL、MongoDB、Redis

CI/CD:GitHub Actions、Kubernetes

我的云服务资源能拿来演示的没那么多,我这里就单个服务分配 512 MB 内存来进行演示

在我们开始进行压测后,不难发现单台服务根本顶不住多少“用户”同时使用。好在我们一开始设计时,就考虑到了扩展的问题。我把服务部署在了 K8S 里面,而且嗯学英语服务是无状态的,那么基于 Service(服务)有状态副本集(StatefulSet)来完成有序的、优雅的部署和扩缩,只需要增加容器组副本数量就行了。

这样一来,客户端不需要关心它们调用了哪个后端副本,通过快速扩容,我们也能容纳更多的请求,现在能够支撑上 w 用户同时使用了。

数据库瓶颈改进

在压测了一段时间后,我们可以发现瓶颈来到了数据库这边。数据库 CPU 占用高、连接数过高(too many connections)、wait timeout 等问题全来了。

我们知道,MySQL 的最大连接数是有一个上限的,但是呢,我们不可能直接设置为上限的那个数,需要根据服务器的 CPU、内存大小等来设置一个相对合理的值。我一般默认 210,最大 1000(别问我能不能再大点儿,得加钱!!!)。

数据库咱们没法像服务那样直接更改部署副本大小就行,要考虑的因素特别的多,毕竟数据才是值钱的东西呢!这里场景的方案就是上集群,一个主实例(读写,也可以多个主实列)和多个辅助实例(只读),这里贴一张 MySQL 文档中的图片:

反映到嗯学英语架构中来,应该是这样的:

初次在 K8S 部署学习 MySQL 集群的话,可以试试 Bitnami 的 MySQL Chart,基于这个入手会更好!

在实际开发中不可能一句上集群就能上的,说起来轻松的事儿,只适合面试吹牛。作为一名开发,还是得先从代码设计层面去解决问题。仔细回想一下,单词数据、错题本之类的数据,是不是都是读多写少的场景?那么我们就没有必要每次都去数据库读了,而是读取之后做缓存,在每次更新数据之后,再更新缓存,这样能降低一些数据库的压力。

连接数解决

这个一般微服务场景下会比较好做一些,要么基于 K8S 的微服务,要么 Spring Cloud,由于我们的嗯学英语前期采用的是 Spring Boot 单体项目,如果用这个方案的话,是需要对架构进行调整的。但是我觉得多邻国肯定不是单体服务,所以还是像拿出来讲讲。

咱们前面不是扩容了很多服务嘛,那么假设每一个服务,我们都分配了最大 50 个连接数的数据库连接池。理论上来说,服务副本数量越多,会占用的数据库连接数也就会变多。

有一句很有名的话,“计算机科学中的所有问题都可以通过另一个间接层次来解决”,据说是大卫·惠勒说的。把这个思路引进来,我们可以发现增加服务副本,是为了缓解核心业务里面对于数据库的“相同”操作,那么我们可不可以把这些操作抽象出来,封装成“间接层”呢?那当然是没问题的了!

在加入了代理服务之后,就能极大地缓解这个问题了。注意哈这一套在微服务里面要好做一些,单体服务弄这个的话,维护起来相对麻烦。

异步

在解决了上述问题之后,假设某一天嗯学英语突然爆火了,10w+ 的用户全部跑来试用(咱们增加压测线程来模拟),咱们的服务器还是崩啦!

虽说大部分场景下,每个用户对数据库中同一条数据的操作相对来说并不多,但是也没法保证出现死锁或者锁竞争的问题。我们的业务,也是在每次用户操作发送请求后,直接入库,等待操作完了之后,才返回的。虽然是第一次做,但是我们可以分析下多邻国看看有没有什么思路。

多邻国在每次完成一个单元的测试后,都会有一个“短暂”的结算动画,甚至有时候去个人档案页面,看到的数据统计和成就,并不是刷新好了的,可能要等个 1~3 秒左右。这类学习类 App 对于用户来说,是具有很高的容忍度的,配合上动画,反而体验看上去也很不错。那么在这中间缓冲的一点时间,就可以让后端服务“慢慢地”去入库就行了,用户感知也就没那么明显了,只需要最大程度的保证数据能保存下来就行。

这里可以在代理服务里面用线程池去处理,或者是用消息队列,然后让代理服务去消费它。在嗯学英语的架构中,我会选择使用 RabbitMQ(消息队列中这个我用的最熟,选其它的也没问题),原因也很简单:我需要能优雅的管理代理服务的集群副本的增加和减少,不能直接更改数量就什么都不管了,而引入消息队列的话,我会省心一些。

消息队列的引入,不仅异步写入降低了一些负载压力,同时也能极大的降低死锁和锁竞争出现的概率。出现这个问题,本质上还是太多的请求同时都来操作这一条数据导致的,我们让请求进入队列,就能避免啦!引入消息队列后,代理服务现在就成为了“消费节点”,我们依旧可以通过线程池或者增加副本的方式,来提升“消费能力”。

除了架构层面,我们在开发时,在代码层面也需要多多注意,应避免全表扫描对表中的所有行记录进行加锁。很多小伙伴开发时可能暂时不会考虑这么多,但是功能完成后,我还是建议看一看执行计划(EXPLAIN)。因为有时候光看 SQL 不一定看得出来,而且不同的数据量执行的 SQL,执行计划也可能差别很大。同时也要对 Slowest queries 进行监控,有剑不用和无剑可用那不是一回事儿。

最后

这就是我对嗯学英语核心业务的思考、设计和改进的一个过程了,当然也有很多应该做的东西也还没做,但实际上“黑天鹅”事件真出现了,也是很难顶得住的。后面继续开发迭代的过程中,我可能也会做出很多调整,等开发完了开源出来,可能也会跟本文所讲的有很大的偏差。

毕竟还有很多内容没考虑进来,比如中间件都挂了怎么办?我要怎么对每一个链路进行“观测”(以及错误追踪)?线上出了问题怎么告警(当然是咱们的老朋友 Prometheus 和 Grafana 啦)?怎么查询日志(ELK 太重了,每个 Pod 都打开看也麻烦)?依托于 K8S,我能以低成本去构建出满足这些的嗯学英语,从而更多时间能放在代码层面去思考。

最后感谢你能看到这里,有想法欢迎与我交流!

时间与人生——我的第一个开源产品经历

前言

前几天发布了 DiyFile v0.5.0 版本,算是做出了一个不算好看但基本能用的版本,今天就来聊一聊整个过程吧。

项目准备

为什么我想做它?

这个原因也很简单,当你学会一个新技术之后,把它用起来,最好的方法就是写一个项目。我也挺想开一个工作室,虽然不知道能不能走出这一步,但是为这个小梦想积累经验还是没问题的。

如何考虑技术选型?

需要学习的东西挺多的,先是画原型设计稿,最开始是用的 Adobe XD 画了一个简单的首页,然后基本上就照着这个设计了。最近简单自学了一下 Figma,后面做产品打算基于它来做了。其次就是项目架构的选型,大白话说就是用什么处理界面、用什么处理服务、用什么存放数据。最开始是用 Nuxt3 写了一版简单的,后来还是被我换成 Vite 项目了,只是没法做服务端渲染了,算是个小遗憾。前端的组件库选型,Arco-Design-Vue、Element Plus、Vuetify3、Naive 都用过,前前后后改了三版,才是现在所看到的样子(Vuetify3 + Naive 混用)。确实也踩了不少坑,浪费了不少时间,但收获挺大。

为什么这么说呢?因为接触了这么多组件库之后,我能在知道某个需求后,正确地选择组件使用了。不知道大家有没有思考过哈,现在让我们回到生产中的需求来。咱们先来想想,UI 原型图出来之后,前后端分别要做什么(分治)?前端根据设计,将 UI 组件分为层次结构,对应着前端每一个细小的组件。后端根据 UI,返回对应的 JSON 结构数据(当然也可能是其它的),咱们这里先不管数据怎么来的,那是业务细节,我们只需要知道,这时候 JSON 数据包含了 UI 原型图上要展示的数据内容。然后(合并),前端根据后端返回的 JSON 结构,映射到页面上的组件结构上,从而完成开发。

为什么说好的设计很重要,设计合理,那么 JSON 结构便合理,也就会自然而然的对应每个组件。UI 和数据模型具有相同的结构,哪怕分了很多层,很多个小组件,也一样能和数据结构中的一部分匹配。这样不仅节约时间(前端再也不用等后端搞完再写了),优秀的设计也能保证逻辑上的 Bug 更少。但是我们都知道,软件开发没有银弹,所以真正干活儿的时候,还是得变通,毕竟这也只是我的一点小想法。

话说回来,为啥我还是放弃 Nuxt3 了呢,虽然有很多插件还在预发布,或者在下个大版本才发布出来,毕竟没有的咱还能自己实现下。真正让我远离它的原因就是:“慢”。不是我故意要诋毁它,我是真没见过不管是启动还是热更新,都比 SpringBoot 2.x 还要慢几倍的东西,我真的越写越烦,再也不想碰这玩意儿了。于是我干脆把后端改成 SpringBoot 3 了,做成前后端分离架构。数据库这边,兼容了 MySQL 和 SQLite 。

总的来说,还是一个权衡取舍的问题吧,不过我选型还有一个要求,就是保证客户能够最大程度的白嫖部署使用。

项目开发

开发流程

开发的流程就比较自由,因为是自己一个人开发的,没那么多讲究。前端基于 Vitesse 来开发,后端就 SpringBoot 3 一把梭了。项目托管在 GitHub 上面,分为了前后端 2 个仓库。然后双分支开发,一个 main、一个 dev,根据实际情况打 Tag 并进行版本发布。

没啥特别好讲的,就聊聊开发中可能要注意的地方吧。前后端都用了代码检查,前端由于引入了 TypeScript,写起来会舒服一些。不过我基本上只解决报错,以及用 eslint 来约束代码风格,毕竟保持一致会减少阅读代码的障碍。后端代码的话,我很喜欢上语法糖,以及引入了阿里编码规约插件和 SonarLint,结合 idea 自带的扫描,基本上够用了,缺点就是很卡(我基本上蓝屏全是因为 idea😭。不过写 Java 代码的时候,我是非常喜欢消灭所有的黄色警告,尽量写的优雅一些,当然如果是大半夜写的状态不好,可能就。。。

代码写完后,就提交并 push 到 GitHub 了,有时候 git commit 我会使用 emoji,因为很好看。然后就会触发 GitHub Actions 来进行代码扫描了,这一步还是有必要的,比如后端会进行一次预构建,保证在切了一个新环境后,代码能正常构建(我碰到过本地成功,CI 失败的情况)。

然后就是自动部署了,review 后,测试也没问题,就可以合并了。

部署流程

部署流程也很方便,就第一次配置稍显麻烦。前端拿 Vercel 来举例子,它可以针对每一个分支和 commit 进行构建,构建完之后,会在 GitHub PR 页面显示 bot 发出来的预览链接,点进去就可以看到效果了。而后端的话,会由 GitHub Actions 自动构建镜像,然后集群内部更新镜像部署(这一步手动自动都可,开源项目可以考虑手动,防止恶意代码被 PR,然后在远程服务器上执行)。

重要的是配置反向代理,以及不同的前端环境,反代到不同的后端环境。数据库生产和测试可以偷懒用同一个,但是基于 PR 的(未合并),建议单独整一个,避免恶意代码执行导致脱裤,泄露 Key。

文档和社区维护

一个产品出来了,肯定得有一个文档,提供给用户查阅。在 DiyFile 发布后,我给它编写了一份文档 ,用户只要会使用部署所需的相关组件,应该是能够照着文档部署的,比如 Linux、Nginx 和 Docker。

社区维护就是比较难的,由于目前没有什么用户群体,也就没有去弄这个。只是在一些论坛发布了推广。

最后

经历了这个个产品后,我也大致了解了一款产品需要具备三大核心:好的创意、有人用、有人反馈。其它的很多东西,我认为都可以归类在这三个里面。没有好的创意,可能开发出来了就没人用,也就不会有人反馈了,支撑你做下去的动力也就越来越小。虽然一个项目生命周期的每一步都至关重要,设计、开发、发布、维护,但也都只是过程。

补充:

有了这次经历(虽然项目还有不少问题需要解决),我学到了很多东西,可能以后在做新项目时,会更加谨慎吧。接下来要沉淀一段时间学技术,为下一个产品做准备!也感谢你能看到这里,希望我的经历对你有所帮助!

时间与人生——编码的意义

其实这篇文章我本打算用“开源的意义”为标题的,但本质上现阶段开源对于我来说,核心就是编码。本文我从一个普通从业者的视角,来聊一聊我的经历、想法与思考。

由来

兴趣

第一次买编程书籍,是 2015 年的时候了,当时还在读高二。我记得是数学选修二还是哪本书来着,里面有一些关于编程逻辑的知识,看完后放假回家就在亚马逊上面买了书。当我把 Java 和安卓的书带到学校时,同学们带来的往往是嘲笑与不解,成绩这么差,怎么会学的好编程呢?这种现实的压力,让我每一步都走的很艰难。但好在当时的班主任是数学老师,我在数学晚自习看书的时候,没有经受任何来自他的“批评”。后面也有女同学问我书上某段代码的意思,促使我形成正反馈,燃起了浓厚的编程兴趣。当时想着,书没读好,就干编程了,兴许以后还能混口饭吃。虽然我后来花了不少精力证明自己的智商能够达到正常人水平,不过这也是太年轻才能干得出来的事儿,现在的我绝对不会浪费时间在这上面😒

第一次接触开源

时间来到 2017 年,高考结束了,不管考的如何,总归是拥有了自己的第一台笔记本了。这一年,我注册了自己的 GitHub 账号,开启了我在开源社区的冲浪之旅。前三年基本上没什么 Activity,但是小小的心灵也遭受了很大的震撼。以往看书,纸质书是没有人帮你勘误的,错了就是错了,可能会把书上的错误当成正确的知识学下去,甚至在验证失败时,会在这儿卡壳,产生放弃的想法。

但来到社区,能看到别人写的好代码学习,纸质书的电子版本也能看到不少人讨论交流,思维不再受到那么深的局限。编程需要你掌握独立学习、独立解决问题的能力,它需要你不断的自我证明,形成正反馈循环,不然你很难坚定的走完前面需要不断学习积累技术的这几年。

分水岭

三年之后

我看了眼 GitHub Contributions 时间线,2020 年是一个分水岭。在这前三年,基本上是以学习为主,但代码敲得少。当然,没吃过猪肉,难道还没见过猪跑吗?我自己不会,肯定得看看别人是怎么做的。你能在 GitHub 上找到很多人,然后打开他们的 Blog,看看他们写下的文字。总结一下就是,我把自己从一个贪玩、不会学习、完全没有自学能力、没有自己思想的人,变成了我现在的样子。当然这跟某个博主启发我的思想,提升思维能力也是有很大的关系的。

一般来说,一个人突然有很大的改变,比较大的可能有两种,要么是他悟道了、要么是他遇到贵人了,某种程度上来讲我属于后者。很多人都说,自己要是在九十年代,也能复刻某乡村教师的创业就好了。成功的人,往往说话只说一半,他们不会告诉你他们遇到了什么贵人、受到了哪些点拨等等。我们要用唯物史观看待问题,你总能找到些蛛丝马迹,比如这位老师的父亲是谁、他为什么能够在九十年代“出访”美国,你那时候是在跟小伙伴快乐的玩泥巴、还是在为了生活奋斗,或者跟我一样还没出生呢?

折腾了三年,迎来了分水岭,为什么是这一年呢?因为这一年我毕业了。

三年之后又三年

在开始参加工作后,我利用空闲时间,以学习为目的,写了一个有一个的“开源项目”,来提升我在编程不同领域的技能。

我是从商城项目开始的,为什么选它呢?因为当时商城、秒杀、高并发吹的很火,想借此学习了解一下相关的知识。但是后来还是把它归档了,因为我发现当时只写的出来后台管理部分,手机端和商城端的页面整不出来,索性不浪费时间在这上面了,毕竟在 Java 这边的学习目的已经达到。

想了解的话可以看这篇文章《我的第一个“正式”开源项目》

再后来,写了一个 SpringBoot 的快速启动框架 Pisces-Lfs 和一个微服务版的解决方案 Pisces-Cloud 。正好在工作上也开始独立负责项目了,业务和技术并进,现在的我独立完成工作上绝大部分开发任务基本上没啥问题了。人有时候还是得逼自己一把,有些东西在工作中用了,但是你只是看,只是在别人基础上改,可能提升效果不大。所以我习惯从 0 开始来一遍,看似代码量不多,但是着其中学习了很多原理,踩了很多坑,帮我积累了很多经验。从无到有的路,该走还是得踏实的走✌️

一些想法

编码为我带来了什么

首先肯定的是,有了一份工作,能有工资拿,可以独立养活自己了。当然,也掉了更多的头发😭我想的是,不管以后编程是否为我的主业,都不能放弃编程(也许能接单赚一点生活费?)。

目前在写的项目是 DiyFile ,虽然目前还不够完善、bug 也多,但是收获也是挺大的。第一次在工作之外做面向 C 端的产品,架构、选型都得自己来。最开始前端用的 Nuxt3,后端用的 NestJS,但是随着复杂度的提升,还是换成 Vue3 和 SpringBoot 了。做这个也是想积累自己的产品经验,比如做什么产品,技术方面怎么选型、组件库用哪些可以更适合要做的东西、以什么样的方式部署、需要适配哪些平台等等。也算是为自己以后开工作室积累经验吧。

用一句话来说,编码改变了我的生活~

工作室我以后肯定要开的,算是一个自己的小梦想吧,目前也是在为了梦想做准备。路要一步步踏实地走,原神中的甘雨,每次捡到宝箱了,都会说:有所收获,就是一件好事。我们又何尝不是呢!

最后

毕业之后,经历了杭州和武汉两个城市的工作。现在觉得,有一份普通的工作不是一件可耻的事情,躺平、逃避、逃跑、奋斗也不是,这只是每个人不同的生活方式。没能在一个公司干下去、那就换一家公司,没能在一个赛道坚持下去、那就换一个赛道,不要勉强自己。虽然会有很多不甘心,心想自己为什么混成了这个样子,但实际上也说不上差。当下的生活充满各种机遇、不顺心,我虽说不上多感激,但至少不嫌弃。

最后,希望大家保重身体,健康度过 2023!

珍珠之歌——我的2022总结

这便是你长路的纪行,这便是你们的珍珠之歌。

题记

这篇年终总结是 2022 年的,当你看到这篇文章的时候,说明 2022 年已经过去了。今年真的过的挺快的,并且放开后还是第一批阳的,在我写这篇总结的时候,我的咳嗽仍然还没好。

原本标题应该叫做《时间与人生——我的2022总结》的,但是原神实在是太好玩了,所以就用了游戏里的【珍珠之歌】来命名了。

只要不失去你的崇高,整个世界都会向你敞开...

回顾

👉 珍珠之歌——我的2021总结

2022 年的年度规划

  • 继续深入学习 Java 语言以及 Spring 全家桶。
  • 学会 React 和 TypeScript,以及周边工具。(React 基础学完后还是放弃了,TS 倒是越来越熟练了,现在以 Vue3 为主)
  • 学习 Python 和 Serverless。
  • 买一台 14 寸的 MBP。
  • 旅游(单人徒步西湖应该算吧)。
  • 脱单。
  • 不仅要有输入,也要有持续的输出。
  • 写一款自己的主题或 web 项目。
  • 过完 2022。

今年主要围绕 Web 开发和云原生学习,对 Java 和 Spring 全家桶的了解加深了,同时还学会了 Kubernetes。我的学习方式跟之前相比,发生了一些变化,所以今年尝试阅读了一些源码和源码解析,主要还是 JDK8、mybatis、skywalking、redis 分布式锁为主。

📚 我的书单

这里统计一下今年阅读过的书单,主要还是上半年读得多,下半年翻文档和源码多一些,也因为工作的原因,没太多时间读书了。当然,并不是所有书都读完了,有些书是在读状态,所以,你可能会在我的 2023 年终总结里面再次看到相同的书:

技术类:
《On Java 中文版 基础卷》——在读
《Java 8 实战》——读完
《Java 测试驱动开发》——读完
《SQL 基础教程 第二版》——读完
《SQL 进阶教程》——读完
《凤凰架构:构建可靠的大型分布式系统》——在读
《重学Java设计模式》——在读
在线电子书:
《现代JavaScript教程》——在读
《Elasticsearch 8.x Cookbook - 第五版》——在读
《高性能 MySQL,第 4 版》——在读
《Learning Modern Linux》——在读
其它:
《球状闪电》——读完
《超新星纪元》——在读

今年读的书不多,平均下来一个月都不到一本,惭愧惭愧...不过话说回来,相较于很多人喜欢看微信公众号之类的学习,我还是更喜欢翻书和文档。或者说点进对应的源码去看,刚开始我也是看不懂,后来我就通过其他人的博客观察,看看他们在碰到类似问题是,基于源码分析的思路。这个思路是很重要的,一通百通,经常这样做下来,就能学会了!大白话就是,看别人总结好的东西没错,但是你要是抛弃了自己学习、实践和总结的过程,那这样学习就没有意义了。

对于学习方式方法这块,哪怕是看我历史的文章也不难发现,在我自己发现或者他人指出问题后,我都会进行相应的调整。这对于我来说还是挺重要的,尤其是看到别人的分享后,我会提取需要的部分,来优化自己的学习模型。

👨‍💻 我的数字生活

开源项目

今年通过 Learning-by-doing 的方式,做了一些个人项目。对微服务的很多场景有了更多的了解,同时也学习了更多的测试和日志配置。对项目开发到 CI/CD 都有接触,也是通过项目学会了在 K8S 上部署微服务。不过 2023 年不打算继续这样写项目了,我主要将重心放在两个方向:做产品、给开源项目提 PR。

Readme Card

这个项目是我写的一个简单的微服务项目,其它项目你也可以访问我的 GitHub 查看。

云服务

今年花了很大的精力迁移个人云基础设施,后端服务几乎全部迁移到 K8S 集群中部署了,因为确实方便,只需要保存好 yaml 文件和备份一些重要数据就行了。镜像构建我全部采用了 GitHub Actions,虽然有一定的弊端,但是免费且性能好。前端和一些代理主要用 Vercel 和 Netlify 部署,云入口则采用了 CloudFlare。各种日志、监控和报警也都配置了,只不过我绝大部分资源全都是白嫖的,可用性相对于付费来说,可能差一些,算是一个取舍吧,但实际上我长期使用下来,没发现什么不好的地方,最多就是架构兼容性需要花精力去适配。

我的现实生活

游玩记录

今年回武汉之前,专门跑去西湖徒步,拍了一些照片。甚至还在西湖电话面试了一家远程工作的,虽然后面就没联系过了。。。西湖风景很多,最大的遗憾是没走到于谦祠。

我整理了一些照片,你可以在我的相册看看。

买了一些东西

今年好像也没买啥了,倒是买了个微型服务器,HPE Gen8,1650 元收来的,加上买的固态之类的,总共花了 1900 元,可以放家里跑一些服务。

人生

看看这一年

6 月份的时候,从上家公司离职。在杭州待了 2 周之后,去了一趟西湖,就回家了。然后 7 月份在武汉面试找工作,不管是在杭州还是武汉,我觉得高质量的面试还是太少了,即使我很菜,但是你面试官问的没水平,也没几个人敢接这个 offer 吧。。。最后还是找了个班先上着,暑假找工作真的是太热了,感觉一出门人都要晒化了。

面试总结可以看这篇文章:《时间与人生——面试随想》

去年报考了专升本,今年也开始在多邻国上面坚持学习英语,效果还是有的,至少我不再抗拒英语了,简单的单词、语法和句子都能弄明白。

12 月份还感染了新冠,我应该算是放开后的第一批了,我不知道是不是我的这个毒株猛一些,过程确实是相当的难受。

未来

2023 年,我希望 Java 这块的技术继续提升,同时前端这边继续学习 Vue3 和 TypeScript。继续学习 Python,可能也会学习一些 Qt for Python 开发。

2023 年,我希望能够学会 Blender 图形软件以及视频剪辑。

2023 年,我想买一个刻晴手办🙂

2023 年,我想买一台自己的相机,可以拍照和 Vlog,记录下我这简单又平凡的生活。

2023 年,希望能脱单!

2023 年,希望能平安健康的度过!

感谢你能看到这儿,新年快乐,在此献上我最忠心的祝愿😊!

基于 Kubernetes 和 SkyWalking 的 Spring 微服务监控实践

前言

最近在哔哩哔哩做了一期直播技术分享,算是人生中第一次吧🙂内容是基于 Kubernetes 和 Skywalking 的 Spring 微服务监控实践,实现 Spring 云原生可观测性。自己排练的时候,准备了一个多小时的内容,结果直播时太紧张,半个多小时就讲完了😂

本来打算用 PPT 整理一些概念和注意点来讲,然后结合项目讲解项目怎么配置、怎么部署,以及请求进来了,怎么通过 Skywalking“观测”请求走过的调用栈,以及查看日志等。由于时间问题,部署这一块就省略掉了,只展示了最终的效果,毕竟部署特别费时间。后面也打算从 0 开始,搭建 K8S,然后部署 nacos、redis、数据库之类的,再把项目和 skywalking 给部署上去。直播录屏正好可以拿来做项目的视频部署教程,也锻炼锻炼自己临场实践能力,以及出问题了怎么查资料去解决。

开播前后有很多朋友的鼓励和支持,这里谢谢大家!本文的内容算是与视频内容的互补。

让我们开始

为什么需要 Skywalking?

在 Spring 微服务场景中开发过的小伙伴应该都了解,我们在排查问题的过程中,往往会因为调用栈太长、跨过的服务太多、请求量太大、无法准确追踪异步线程等,浪费大量的时间。这种情况并不像单元测试对功能性方法测试一样,很方便的就能看到报错信息以及 Debug,我们需要有一种方式,帮我们追踪每一个”请求“线程,来实现”观测“。

拿图中的架构来举例子,请求打到外部网关入口,然后被转发到 Spring-Gateway 所在的 ServiceNodePort,对应的流量会负载均衡到每一个这个 Service 内的 Pod。然后 Spring Gateway 会根据路由,将流量转发到对应的业务服务。业务服务可能会操作数据库、redis 以及消息队列之类的中间件,最终处理完之后,将响应回对应的请求。而这个过程产生的日志和监控数据,将会被上报给 Skywalking OAP,我们访问 Skywalking UI 的 Service 就能看到界面了,通过各个图表获取我们需要的信息。

程序在出现异常时,会将 traceId 一并响应给请求方,这样就可以通过 traceId 去 Skywalking 查询相应的数据了。在上线前,它有一个典型的案例场景:测试自己复现了 Bug 之后,拿到 traceId 给开发,开发去 UI 查看日志和数据。极大地降低了测试和开发人员间的友好互动频率、维护了团队的良好氛围形象、提升了 bug 响应修复的速度,同时避免了在测试电脑上有 bug 但在开发电脑上世界和平的现象(笑死

注意:这里每个负载间都是通过 Service 的虚拟 IP 来通信的,不能够使用 Pod IP,因为重新部署后 IP 会发生变化,同时也将无法做到负载均衡了。

Skywalking 是什么?

SkyWalking 是一个开源可观测平台,用于收集、分析、聚合和可视化来自服务和云原生基础设施的数据。SkyWalking 提供了一种简单的方法来保持分布式系统的清晰视图,甚至跨云。它是一种现代 APM,专为云原生、基于容器的分布式系统而设计。

SkyWalking 为服务(Service)、服务实例(Service Instance)、端点(Endpoint)、进程(Process)提供可观察性能力。

服务可以映射成我们的 Spring 的每一个服务,在 Kubernetes 中也就是 Service,而服务实例对应着 Service 内的 Pod(举个例子:咱们的 Spring Gateway 网关”服务“,可以启动多个实例对吧?)。而在开发中我们最常关注的是端点,端点用于传入请求的服务中的路径,例如 HTTP URI 路径或 gRPC 服务类 + 方法签名。

举个例子,前端请求后端的登录接口,http://127.0.0.1:8000/pisces-admin/user/login,就可以是一个 endpoint,这个 endpoint 的内容是 POST:/user/login

SkyWalking 在逻辑上分为四个部分:Probes、Platform backend、Storage 和 UI。作为开发,我们最需要关注 的是 probes 和 ui,probes 可以理解为程序中的“探针”,而 ui 是我们“观测”数据指标的界面。

如何收集 Java 程序的数据?

介绍完 Skywalking 的一些概念之后,我们最关心的肯定就是如何从 Java 程序中收集数据。刚才我们说到了”探针“,我们就是要靠它来实现。

在 SkyWalking 中,探针是指集成到目标系统中的代理或 SDK 库,负责收集遥测数据,包括跟踪和指标。

SkyWalking Probes 可以分为四种不同的类别:

  • Language based native agent,也就是基于语言的原生代理
  • Service Mesh probes
  • 3rd-party instrument library
  • eBPF agent

显而易见,我们要用的就是针对于 Java 的基于语言的原生代理,也就是 Skywalking Java Agent,它为 Java 项目提供本机跟踪/指标/日志记录/事件功能。

如何使用

在项目中使用 skywalking agent

本文以 logback 日志实现为例,首先我们要将下面的包引入进我们的项目:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId>
<version>${skywalking.version}</version>
</dependency>

注意:只要你的微服务项目配置了全局的 spring-cloud 版本,那么这里的 sleuth 就不需要单独指定版本,它会默认继承你当前 spring-cloud-dependencies 指定的版本。

  • logback-spring.xml 中添加 gRPC reporter.
1
2
3
4
5
6
7
<appender name="grpc-log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCPatternLogbackLayout">
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{tid}] [%thread] %-5level %logger{36} -%msg%n</Pattern>
</layout>
</encoder>
</appender>

注意,name 参数可以自定义,但是记得加上 appender

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds">
<!-- gRPC reporter 在这一层级 -->
<root level="info">
<!-- 就是下面这一行 -->
<appender-ref ref="grpc-log"/>
</root>
</configuration>
  • 下面这行插件的配置,如果不添加的话,会使用默认值:
log.max_message_size=${SW_GRPC_LOG_MAX_MESSAGE_SIZE:10485760}

到这里,我们已经配置好基本的内容,这下你的服务已经可以上报数据了。

Skywalking 服务配置

程序中配置好了,那么我们把数据上报到哪里,以及如何查看呢?这里以开发环境为例子,毕竟如果在本地开发的话,还是需要调试的。

  • 安装 skywalking

我们先去官网下载 Skywalking APM,下载完之后解压,本地我们不做特殊配置,直接双击 apache-skywalking-apm-bin\bin\startup.bat 就可以启动了。启动之后,访问 http://localhost:8080,就可以看到页面啦!

启动完之后进去是没有数据的,等程序上报数据就好了。

  • 安装 skywalking java agent

我们需要在程序启动时,让 agent 以插件的形式加载进来,当然也需要下载这些包了。去官网下载 Skywalking Java Agent,然后解压之后得到 skywalking-agent 文件夹。点进去之后我们可以看到一个 jar 包 skywalking-agent.jar,记住这个包在你硬盘上的绝对路径。

由于 Spring Gateway 是基于 WebFlux 的,所以我们要添加 2 个插件进来。进入 skywalking-agent\optional-plugins 文件夹,找到如下 2 个 jar 包:

apm-spring-cloud-gateway-3.x-plugin-8.13.0.jar
apm-spring-webflux-5.x-plugin-8.13.0.jar

然后复制到 skywalking-agent\plugins 文件夹下就行了。

注意,具体的版本,取决于你当前所用的版本!

idea 配置

接下来我们去 idea 中配置,以便让 idea 中的 Java 服务启动时能顺利加载插件。

我们先打开 Run/Debug Configurations,然后找到我们对应的服务(没有的就添加),选择 Modify options,勾选 Environment variablesAdd VM options 选项。

然后添加对应的启动参数:

第一个红框框的内容,是 JVM Options 参数:

-javaagent:D:\env\skywalking\skywalking-agent\skywalking-agent.jar

-javaagent: 后面的是你开发环境的 skywalking-agent.jar 包的绝对路径位置。

第二个红框框,是配置 SkyWalking Agent 的环境变量:

SW_AGENT_NAME=pisces-gateway;SW_AGENT_COLLECTOR_BACKEND_SERVICES=127.0.0.1:11800

SW_AGENT_NAME 的值填写你要指定的 Service Names,如果你需要同时配置 Service Groups,你可以这样写:

SW_AGENT_NAME=Pisces-Cloud::pisces-gateway;

:: 符号前面的就是 Service Groups,后面的就是 Service Names

SW_AGENT_COLLECTOR_BACKEND_SERVICES 的值填写你的 SkyWalking OAPAgent Backend Service 的地址。

图表信息

直播时基本上都介绍过了,这里挑几个典型大致讲讲吧。

图中我们看到的是服务概览,以及整个服务链路的拓扑图。当然,关于链路追踪重要的部分,还得是 Trace 和 Log,下面我以 POST:/user/login 这个登录的端点来举例子。

调用链路信息

我们可以看到每一个上报过的端点,以及端点对应的信息。图中端点的请求走过的每一个不同的服务,都是用了不同的颜色区分开了。而竖着的直线,就是对应着当前的服务,中间的每一个跨度都能点开查看日志,包括执行的 SQL 语句,然后只要输出了日志信息,都可以关联到对应的 LOG,是不是特别方便?下面放一张全貌图。

服务业务数据图表

图中的注释已经说的很详细了,有一点需要注意的是,消息队列相关的那 2 个图表,需要你当前服务具有”消息消费者“的时候,才会显示数据的,毕竟是展示 Consuming 数据嘛。

JVM 数据图表

每一个实例进去,都能够看到对应的 JVM 的信息,毕竟有时候需要诊断 JVM 数据来定位问题嘛。我们可以看到有内存/CPU占用情况,新生代、老年代的 gc 时间和次数,各种状态的线程(守护线程、阻塞线程、可运行线程、等待线程)次数啦等等。

毕竟对于初学者来说,学习使用各种工具去查看这些指标,是需要一定的精力的。尤其是碰到线上不给任何权限的那种,这时候程序能够主动上报数据,我们就能直观的看到了。

注意,线上环境的任何观测/上报数据的接口,都不建议对外公开访问,做好白名单/黑名单机制拦住它。

开发中的注意点

跨线程追踪

程序中教程会碰到,需要用异步线程处理的情况,那么这种时候如果需要对异步线程进行追踪的话,可以采用跨线程追踪方案。

  • 首先我们先引入包:
1
2
3
4
5
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>${skywalking.version}</version>
</dependency>
  • 拿下面这个场景来说,我需要发送一条消息:
1
2
3
CompletableFuture.runAsync(() -> {
messageSender.sendBark(String.format("时间:%s,用户:%s 登录系统!", LocalDateTime.now(), account));
});

这种写法是无法追踪到的,在引入包之后,我们可以这么写:

1
2
3
CompletableFuture.runAsync(RunnableWrapper.of(() -> {
messageSender.sendBark(String.format("时间:%s,用户:%s 登录系统!", LocalDateTime.now(), account));
}));

值得注意的是,CompletableFuture.runAsync() 是没有返回值的,所以用的是 RunnableWrapper.of(),如果我们用的是 CompletableFuture.supplyAsync() 的话,就需要换成支持返回值的 SupplierWrapper.of() 方法。

全局异常处理

全局异常处理这基本上业务开发都会用到,但是我们抛给前端的信息中,需要带上 traceId,这样不管是用户还是测试,都可以根据报错信息中的 traceId 反馈问题,排查也就减轻了一些负担。

  • 首先引入相关的包:
1
2
3
4
5
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>${skywalking.version}</version>
</dependency>
  • 通过 Skywalking 手动 API,我们可以获取到 traceId:
1
String traceId = TraceContext.traceId();
  • 然后加到异常处理中响应给前端:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 全局异常拦截 handleException
*/
@ResponseBody
@ExceptionHandler(Exception.class)
public CommonResult<?> handleException(Exception ex) {
log.error("全局异常信息.[异常原因={}]", ex.getMessage(), ex);
return CommonResult.failed(HttpStatus.ERROR, "系统异常,请联系管理员!", null,"traceId=" + TraceContext.traceId());
}
}
  • 响应的异常信息如下:
1
2
3
4
5
6
{
code: 500,
message: "系统异常,请联系管理员!",
data: null,
traceId: "traceId=7d2705aebedb40a1985af4ed20f569b0.510.16686682751090531"
}

traceId 生成规则

生成的 traceId 一大串,怎么读懂它?(虽然只是个符号而已,但是我们也可以了解下)

先看源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private static final String PROCESS_ID = UUID.randomUUID().toString().replaceAll("-", "");
private static final ThreadLocal<IDContext> THREAD_ID_SEQUENCE = ThreadLocal.withInitial(
() -> new IDContext(System.currentTimeMillis(), (short) 0));
public static String generate() {
return StringUtil.join(
'.',
PROCESS_ID,
String.valueOf(Thread.currentThread().getId()),
String.valueOf(THREAD_ID_SEQUENCE.get().nextSeq())
);
}

我们知道,traceId 是由 2 个点分隔开的一串长字符串。第一个点前面是生成的 UUID,并替换掉了 - 符号;中间的三个数字,是当前线程的线程 ID;最后的内容是根据时间戳 * 10000 + 当前线程中的 seq 生成的。UUID 生成后就不会变了,线程 ID 也是一样。

注意,这里的线程 ID,并不是请求线程的线程 ID,说它是当前实例的生成器的线程 ID 更合适。而且 UUID 和线程 ID,每一个不同的实例生成的也是不同的,只是每一个实例下相同。这点其实也很好验证,每个 Service 启动多个实例,然后来一波多线程压测就显而易见了。

最后

这是第一次做直播技术分享,也非常感谢大家的支持!虽然第一次看的人不多,但是也留下了录屏,而且依旧可以写博客发出来,算是一次不错的经历,以后也打算继续做直播技术了,偶尔直播技术分享,同时也欢迎大家多多跟我交流技术呀!

Dynamic k8s 集群实现方案

前言

本文要探讨的,是一种极低成本,但是可用度较高的 K8S 集群方案,提供多种实现思路,以及部分可实现方案。我踩过一些坑,在现有资源下,其实有很多种玩法,但如果有小伙伴问:你怎么不用 xxx 家的 xx 服务啊?怎么不用 xxx 方式啊?

说到底,如果你是为了“生产级”而考虑,那确实也没问题,可靠性最重要。但我这种,贯彻的宗旨是“白嫖”,比较适合自己折腾当玩具、开源项目演示站之类的。比如生产级会在集群外配置 LoadBalancer,但是我就不会考虑,因为要花钱。而且我还得考虑,其中一个节点集群挂了,或者号没了,我能在极短的时间内切换到正常的集群上去。

方案

思路

我先来解释下这张图的意思吧~我是用 Pisces-Cloud 项目为例,因为是个前后端分离的微服务,所以前端直接放在 vercel。因为我觉得前端资源就没必要放在集群里了,而是应该放在 cdn 上,让用户能最快的访问静态资源,同时降低后端的压力(毕竟服务器只需要处理 API 请求就行了。

然后域名是放在 Cloudflare 进行解析的,目前是直接解析到集群的 Nginx 入口,再进入集群内部,通过 Service 生成的 Virtual IP,提供一个 NodePort 来访问,Nginx 对这个 NodePort 进行反向代理。

注意,在这一步的 Virtual IP 需要创建无状态负载来实现,因为默认的应该是 Headless,只供集群内部使用。

k8s 里面,有多种方式可以向外暴露进行访问,比如 hostNetWork、hostPort、NodePort、LoadBalancer、Ingress 等。我之前用的是 Ingress 的域名路由方式,对 L7 进行转发,但这种方式有个小问题,配置好的域名后面还要带端口号访问,就很别扭。后来索性直接改了下 Ingress Controller 的端口,改成 80 和 443 了,那样就去掉了端口号。

这几天迁移后,就按照图片上的方式,放在集群外的那台 Nginx 上面的,证书也是在上面配置的。至于 Workers 那一步,咱们放到后面讲,目前我还没上,只提供思路,哪天有空弄好了补充完整的。

然后就是整个后端服务这边,各种配套的监控、中间件,都是在集群内部配置的。由于可以直接用 Helm,在配置一主三从的时候,反而比用 Docker 跑方便的多,毕竟改改 yaml 文件就好了嘛(🐶

我的实现

这套玩法里面最重要的,是 Cloudflare 到集群这一块。第一张图片上 Host-0、Host-1 这些,就是不同的集群。可能有小伙伴要好奇了,为什么要弄多个单独的集群,而不是一个 master 对应多个 node 呢?我之前是这么玩的,但是我这个机器是白嫖的 Oracle Cloud 的 4C24G 的 ARM 机器,一个号只能开一个,所以走不了一套内网,而且跨地域延迟也高,时不时还调度失败。。。虽然可以通过公网把几台机器组成内网实现,但是延迟是个大问题,并且时不时丢包。

除了折磨人,可用性方面还是可以的。比如 Host-0 挂了,我直接把域名解析到 Host-1,服务就能正常使用了。数据同步,之前阿里云搞活动卖 20 一年的 RDS,拿来干这个不错,但现在已经过期了,因为要求不高,我是定时手动用 Navicat 去同步备份的。这勉强算是一种毕竟 Low 的异地容灾吧。。。宕机时间从收到告警邮件到登录 Cloudflare 切换 DNS 解析为止,花不了多久。

你要问我最大的缺点是什么?我觉得应该是限制于 ARM 的架构,导致基于 Jenkins 的 DevOps 系统安装困难(不是不能哈,兼容性问题需要解决。

基于 Cloudflare Workers 的动态负载均衡

这个思路呢,我是打算基于 Reflare 这个项目做的。用 Cloudflare Workers 本身做反向代理,应该问题是不大的,我自己也用了很久了,而且这玩意除了反代,搭建网站,搞负载均衡本身也是支持的。

Reflare 有个实验性的功能,叫“Dynamic Route”,也就是动态路由。实际上是将路由基于 Worker KV 存取。这样的话,是不是可以对集群进行监控,然后触发制定的规则后(比如30秒一次,失败5次以上),通过 Cloudflare API 修改路由呢?(这个项目的仪表板正在开发中)这里直接用 Redis 替代 Worker KV 也是可以的。

使用 cloudflared 将 Kubernetes 应用程序公开到 Internet

这里是推友提供的思路,原本只知道能搞内网穿透,没想到竟然也能干这个活。

具体思路应该是这样:把 cloudflared 部署到 k8s 中,那么就能用 Tunnel 创建连接了。注意文档上说“同一个 Tunnel 可以从 的多个实例运行 cloudflared,使您能够运行多个 cloudflared 副本以在传入流量发生变化时扩展您的系统。”。假设这时 k8s 集群挂了,那么上面运行的 cloudflared 实例也就无法和 Tunnel 通信了,反过来 Tunnel 也就无法将流量代理到这个 k8s 集群了。比我上面说的基于 Workers 的要容易实现一些,毕竟可以直接将流量路由到 Cloudflare 的负载均衡器了。

果然还是大家骚点子多啊,正好国庆放假在家把这个折腾好🙃

最后

文章可能有点水了🙃不过这里主要还是提供一种思路,看到这篇文章的你,在生产环境千万别这么玩......如果你像看具体的操作的话,这个项目的文档里面,我后面会抽时间写完这部分。不过比较熟悉 k8s 的小伙伴,其实看完就大致明白了。

虽然俺是 Java 开发,但是万一咱下一家公司就用 k8s 呢是吧?(其实现在这家就在用)不是说要去学怎么做 k8s 开发,但是至少得了解咱们开发的程序在 k8s 上部署的大致流程。虽然生产都是运维去搞,但是咱们熟悉的话,在查问题时会更从容🙂

参考资料:

权限模型之基于角色的访问控制(RBAC)

前言

在搞明白 RBAC 这个访问控制模型之前,我们得先了解下,信息安全中的一些基本的概念,从而了解 RBAC 属于哪一个环节。

  • 认证(Authentication):系统识别操作用户的真实身份,也就是弄清楚操作用户到底是谁。
  • 授权( Authorization):系统控制一个用户操作数据的权限,也就是控制操作用户在系统中能干什么。
  • 凭证(Credential):系统承载和用户的认证授权信息,你需要一个凭证,来证明你就是你,然后就可以干允许你干的事了。
  • 机密(Confidentiality):系统确保凭证或数据传递与存储的隐秘性,避免被篡改或者泄露。
  • 传输(Transport Security):确保系统和用户之间的网络传输信息无法被第三方窃听、篡改。
  • 验证(Verification):系统确保提交到每项服务中的数据是合乎规则的,乱填一些数据,是可能导致系统安全问题的。

那么 RBAC 是解决什么问题的呢?也就是“(User)拥有什么权限(Authority)去操作(Operation)哪些资源(Resource)”,所以本质上属于一种授权(访问控制)行为。

RBAC

什么是 RBAC?

基于角色的访问控制(Role-based access control,简称 RBAC),指的是通过用户的角色(Role)授权其相关权限,这实现了更灵活的访问控制,相比直接授予用户权限,要更加简单、高效、可扩展。

怎么理解 RBAC?

这里我画了一张图,图中的贝丝是管理员,所以他具备了对资源 A 和资源 B 的可读、可写资源。而莫斯作为开发者,他对资源 A 具有可读、可写权限,但是对资源 B 只有可读权限。这样我们就满足了“(User)拥有什么权限(Authority)去操作(Operation)哪些资源(Resource)”的行为。但是显然有个不好的地方,让我们接着往下说。

这时来了一个新的开发人员塔斯,想加入我们一起开发,由于他的职责和莫斯是一样的,所以我们给他分配了同样的权限。细心的你可能已经发现了,给莫斯和塔斯分配权限是一个重复的步骤,看上去并没有什么不妥。

那么我们要想象一种糟糕的场景,如果我们需要对开发人员分配许多的权限,而且开发人员同时又有很多该怎么办?想必贝丝管理员这时一定在抱怨了!🥲如果我们这时要删除所有开发人员对资源 A 的可写权限,也需要一个个更改,这简直坏透了🤬

这时我们可以考虑用一种通用配置的思路,而不是直接给用户分配这些权限。如图所示,我们只需要给贝丝分配 admin 配置,给莫斯和塔斯分配 dev 配置即可。如果下次再有小伙伴要参与进来,直接分配给他 dev 配置就可以了。

我们在系统中通常把这些配置文件叫做“角色”,这也就是基于角色的访问控制的方式,还是很好理解的,对吧?

软件开发中的 RBAC

在软件开发中,普遍采用的 RBAC 模型,是 NIST RBAC 模型。咱们上面说的,为了避免对每一个用户单独设置权限,以及避免过程中容易产生的错误,将权限从直接绑定用户,改为了绑定“角色”。将权限控制变为了对“角色拥有操作哪些资源许可”。

这里的“许可”可以理解为抽象权限的具象化体现,角色解耦了用户和权限之间的多对多关系,而许可是用来解耦操作和资源之间的多对多关系的。比如对资源 A 可以有增、删、改、查等操作。

注:1996年,莱威·桑度(Ravi Sandhu)等人在前人的理论基础上,提出以角色为基础的访问控制模型,故该模型又被称为RBAC96。之后,美国国家标准局重新定义了以角色为基础的访问控制模型,并将之纳为一种标准,称之为NIST RBAC。

在软件开发中,如果要实现 RBAC 模型,则需要实现它的几个核心概念:

  • 用户:访问系统的任意用户。
  • 角色:角色是一个逻辑集合,你可以授权一个角色某些操作权限,然后将角色授予给用户,被授予角色的用户将会继承这个角色中的所有权限。
  • 资源:应用系统中的实体对象一般定义为资源,比如订单、商品、文档等等,而每种资源都可以定义多个操作,比如商品有查看、编辑、上架、删除操作;
  • 授权:把某类(个)资源的某些(个)操作授权给角色或者用户。

注意“授权”这一项,我给“或者”加粗了,为什么这里用“或者”呢。因为有些软件开发框架在设计时,并没有完全遵循 RBAC 来设计,比如 Spring Security...不过并不是说这样设计不好,首先怎么用是用户自己的事,可以完全遵守 RBAC 来用,而 Authority 在某些场景下,它也是适用的。

操作系统中的 RBAC

在 Linux/Unix 系统中,RBAC 用于控制用户对通常仅限于超级用户的任务的访问。通过对进程和用户应用安全属性,RBAC 可以向多个管理员分派超级用户功能。进程权限管理通过特权实现。用户权限管理通过 RBAC 实现。

我们都知道,root 用户在 Linux 系统中,是具有最高权限的,是基于叫做超级用户模型的(管理员对系统要么具有全部控制权要么毫无控制权)。而使用 RBAC,可以在更精细的级别上强制执行安全策略。RBAC 采用最小特权的安全原则。最小特权表示用户仅具有执行某项作业所必需的特权。普通用户具有足够特权来使用其应用程序、检查其作业状态、打印文件、创建新文件等。超出普通用户功能以外的功能将分组到各权限配置文件中。如果用户将要执行的作业要求具有某些超级用户功能才能执行,这些用户将承担拥有适当权限配置文件的角色。

RBAC 会将超级用户功能收集到权限配置文件中。这些权限配置文件将指定给称为角色的特殊用户帐户。然后,用户可以承担某个角色,以执行要求具有某些超级用户功能才能执行的作业。

参考资料: RedHat 基于角色的访问控制 (RBAC)

Kubernetes 中的 RBAC

在 Kubernetes 中,我们通常授予对 API 端点(endpoints)的访问权限。RBAC 通过允许或禁止访问管理 API 资源来定义用户、组和进程的策略。

在 Kubernetes 中,可以用 RBAC 做哪些操作呢?比如:

  • 为系统的不同用户,定义不同的角色,来访问不同的 Kubernetes 资源。
  • 控制 Pod 中运行的进程,以及可以通过 Kubernetes API 执行的操作。
  • 限制命名空间的资源可见性等等。

在 Kubernetes 中,RBAC 由三个模块组成,它们一起将 API 原语(API primitives)及其允许的操作连接到用户、组或者 ServiceAccount.

Kubernetes RBAC API

我们来了解下实现 RBAC 功能的 Kubernetes API 原语:

  • 角色:角色 API 原语声明了该规则可以操作的 API 资源及其操作(查看、删除等)。
    • 允许查看和删除 Pods
    • 允许查看 Pods 的日志
  • RoleBinding:RoleBinding API 原语将角色绑定到主题。例如:将允许更新服务的角色绑定到用户 贝丝。

所有的 Kubernetes 集群都有两类用户:Kubernetes 管理的 Service Account 和普通用户。角色可以分配给外部用户或机器人(集群内的应用程序)。我们来总结一下,完成访问控制的行为,需要的几步操作:

  • 创建/拥有一个用户。
  • 创建角色以向 API 端点授予权限。
  • 将角色链接到用户。

最后一步称为绑定,由上图中名为 RoleBinding 的对象完成,该对象将身份链接到角色。

注:Roles 和 RoleBindings 在命名空间中创建,它们授予对当前命名空间中资源的访问权限。如果需要将角色定义为全局而不是限定为命名空间,需要 ClusterRole,要将身份链接到全局角色,需要使用 ClusterRoleBinding

最后

RBAC 访问控制模型,应该是开发中比较常见的模型了,我采取图文的形式,结合自己的理解,写下了这篇文字,希望能对看到的小伙伴提供帮助!😊

我的水平和认知都是有限的,如果你想了解更多关于 RBAC 或者访问控制模型的知识,我为你准备了一些参考资料链接: AWS 的访问控制模型 Wiki-基于角色的访问控制 Kubernetes 使用 RBAC 鉴权 基于角色的访问权限控制 (RBAC) 概览 Azure 的访问控制模型

Spring Cloud on GitHub Actions

GitHub Actions

概览

GitHub Actions 是由 GitHub 推出的持续集成服务。可以创建工作流程来构建和测试存储库的每个拉取请求,或将合并的拉取请求部署到生产环境。GitHub Actions 不仅仅是 DevOps,还允许您在存储库中发生其他事件时运行工作流程。 例如,您可以运行工作流程,以便在有人在您的存储库中创建新问题时自动添加相应的标签。GitHub 提供 Linux、Windows 和 macOS 虚拟机来运行工作流程,或者您可以在自己的数据中心或云基础架构中托管自己的自托管运行器。

它能做什么?

GitHub Actions 支持绝大多数你所熟知的语言,你可以用它进行代码检查、测试、CI/CD、项目管理,甚至能跑 Kubernetes 和薅羊毛。。。

如果我们要搜一些常见的操作,一般会想到 awesome actions,你也可以在 GitHub Market 搜索。

核心概念

主要讲 3 点:工作流程、作业、步骤。其它的内容,请自行翻阅 文档。不过话说回来,只需要了解这 3 个概念就够了,知道了它是怎么运行的,你就能在脑海中构思你每干一件事需要哪些步骤,然后直接去搜索有没有现成的步骤可以使用,当然,是在没有解决方案,只能自己造轮子了。

先来讲讲工作流程,我们要在 GitHub 上面做操作,肯定得有一个仓库,这是最基本的条件。其次,你也许以前就注意到了,很多仓库里面,都有 .github/workflows 文件夹,这是 GitHub Actions 必须的要求。文件夹下,存放的就是 YAML 文件了,实际上一个工作流程,就是对应一个 yaml 文件的。

比如说,我新建了一个 github-actions-demo.yml 文件,那么就相当于创建了一个名为 github-actions-demo 的工作流程。当然,名字是可以在 yaml 文件中配置的。

这是我从文档中抠下来的图,根据这张图我们来理解一下这 3 个概念。“工作流程”是一个可以配置的自动化过程,并且是由 YAML 文件定义的事件触发时运行,或者可以手动触发以及定时触发。我们可以看到,工作流程中是可以包含多个“作业”的,也就是 jobs,比如你可以定义 job1job2 ...每一个作业中,也可以存在多个“步骤”,可以是自定义的脚本,也可以是其它操作等等。

这里提到了一个新的概念,叫做“事件”。如果不好理解,那么我就说 6 个字解释下:“事件触发流程”。比如说你可以定义有人创建 pr、打开一个 issue、推送到存储库,或者是定时运行,来触发工作流程的运行。

这里说一个容易被误解的点,官方的图上看上去每个作业是顺序运行的,但它们也是可以并行运行的哦~😊

理论上来说,在耗尽 GitHub Actions 限定的资源和时间之前,单次工作流程,是可以一直跑的,那可玩性也就大大提升啦!虽然很多骚操作不太推荐就是了,毕竟是免费资源,不能滥用😂

说了这么多,来进入正题吧,啰嗦半天主要是怕搜到这里的朋友,没弄明白 GitHub Actions 是怎么回事,已经轻车熟路的朋友跳过就行啦!

Spring Boot

在 Spring Cloud 之前,我们需要先讲一下 Spring Boot 的操作,毕竟也是基于它的嘛。这里就用基于 Spring Boot 的 Pisces-Lfs 项目来举例。由于我们要打包基于 Docker 的镜像,那么得先写好 Dockerfile 文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 该镜像需要依赖的基础镜像
FROM openjdk:8-jdk-alpine
# 设置环境变量
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms512m -Xmx512m" SPRING_CONFIG="-Dspring.config.location=/root/lfs/application-docker.yml"
# 设置时区
RUN set -eux; \
 ln -snf /usr/share/zoneinfo/$TZ /etc/localtime; \
 echo $TZ > /etc/timezone
# 创建文件夹
RUN mkdir -p /root/lfs
# 拷贝 jar 包,并重命名
COPY lfs-admin/target/lfs-admin-1.0.jar /lfs-admin.jar
# 指定 docker 容器启动时运行 jar 包
ENTRYPOINT exec java ${JAVA_OPTS} -jar ${SPRING_CONFIG} /lfs-admin.jar
# 指定维护者的名字
MAINTAINER besscroft

然后我们再创建 docker-buildx.yml 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 工作流程名称
name: "Java CI with Multi-arch Docker Image"

# 指定事件触发条件和分支
on:
 push:
 branches: [ main ]
 pull_request:
 branches: [ main ]

# 一个作业
jobs:
 docker:
 name: Running Compile Java Multi-arch Docker Image
 # 基于 ubuntu-latest 运行时
 runs-on: ubuntu-latest
 steps:
 # 设置 Java 环境的操作
 - uses: actions/checkout@v3
 - name: Setup Java
 uses: actions/setup-java@v3
 with:
 # 指定 jdk 版本
 java-version: '8'
 # 注意,这里的 adopt 是因为我需要用这个,你可以根据自身情况换成其它发行版
 distribution: 'adopt'
 cache: 'maven'
 - name: Build with Maven
 # maven 构建
 run: mvn -B package -Dmaven.test.skip=true --file pom.xml
 - name: Login to Docker Hub
 # 登录 docker 仓库
 uses: docker/login-action@v1
 with:
 username: ${{ secrets.DOCKERHUB_USERNAME }}
 password: ${{ secrets.DOCKERHUB_TOKEN }}
 - name: Set up QEMU
 # 设置 QEMU,以便支持多平台构建
 uses: docker/setup-qemu-action@v1
 - name: Set up Docker Buildx
 id: buildx
 uses: docker/setup-buildx-action@v1
 - name: Build and push
 id: docker_build
 uses: docker/build-push-action@v2
 with:
 context: ./
 # 指定 Dockerfile 文件位置
 file: ./Dockerfile
 # 打包支持的镜像架构,因为我需要 aarch64 的镜像,所以这里加上了 arm64
 platforms: linux/amd64,linux/arm64
 push: true
 # 镜像标签
 tags: ${{ secrets.DOCKERHUB_USERNAME }}/lfs:latest

工作流程中,有使用到环境变量的地方,也就是用户名和 token,这个不可能直接放在文件中的,不然就泄露了。一般我们存放在 Actions secrets 中,使用环境变量配置进行读取,如下图所示。

Spring Cloud

了解了 Spring Boot 如何操作后,Spring Cloud 实际上也就很简单了。我用自己的 Pisces-Cloud 项目来举例,一个微服务下的子模块,假设全部在一个项目空间内的话,通常来说,打包构建只需要一次,就可以将所有的服务构建完成,那么问题就好办多了。

同样的,我们要对每个服务创建一个 Dockerfile 文件,然后再新建一个 docker-buildx.yml 文件即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
name: "Java CI with Multi-arch Docker Image"

on:
 push:
 branches: [ main ]

jobs:
 docker:
 name: Running Compile Java Multi-arch Docker Image=
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v3
 - name: Setup Java
 uses: actions/setup-java@v3
 with:
 java-version: '8'
 distribution: 'adopt'
 cache: 'maven'
 - name: Build with Maven
 run: mvn -B package -Dmaven.test.skip=true --file pom.xml
 - name: Login to Docker Hub
 uses: docker/login-action@v1
 with:
 username: ${{ secrets.DOCKERHUB_USERNAME }}
 password: ${{ secrets.DOCKERHUB_TOKEN }}
 - name: Set up QEMU
 uses: docker/setup-qemu-action@v1
 - name: Set up Docker Buildx
 id: buildx
 uses: docker/setup-buildx-action@v1
 # 从这里开始看
 - name: Build and push admin
 id: docker_build_admin
 uses: docker/build-push-action@v2
 with:
 context: ./
 file: ./pisces-admin/admin-boot/Dockerfile
 platforms: linux/amd64,linux/arm64
 push: true
 tags: ${{ secrets.DOCKERHUB_USERNAME }}/pisces-admin:latest
 - name: Build and push auth
 id: docker_build_auth
 uses: docker/build-push-action@v2
 with:
 context: ./
 file: ./pisces-auth/Dockerfile
 platforms: linux/amd64,linux/arm64
 push: true
 tags: ${{ secrets.DOCKERHUB_USERNAME }}/pisces-auth:latest
 - name: Build and push gateway
 id: docker_build_gateway
 uses: docker/build-push-action@v2
 with:
 context: ./
 file: ./pisces-gateway/Dockerfile
 platforms: linux/amd64,linux/arm64
 push: true
 tags: ${{ secrets.DOCKERHUB_USERNAME }}/pisces-gateway:latest

注意到区别了吗?实际上只多了“步骤”,就是对每一个 Dockerfile 都创建一个单独的“步骤”,来进行镜像的打包,只需要将这些步骤全部放到 maven 构建之后即可。

一些建议

我这里只提供了能同时满足 x86 平台和 arm 平台的容器构建方案,其它功能其实是由 Docker 本身实现的。

1
2
3
4
5
docker run -d --name pisces-gateway \
 -p 8000:8000 \
 -e JAVA_OPTS="-Xms512m -Xmx512m -Duser.timezone=GMT+08 -Dfile.encoding=UTF8" \
 -e SPRING_CONFIG="--spring.profiles.active=prod --spring.cloud.nacos.discovery.server-addr=http://127.0.0.1:8848" \
 besscroft/pisces-gateway:latest

比如说这里 Docker 容器启动时设置 JVM 参数,以及配置参数。

对于发布版本镜像,开源社区中比较规范一点的玩法是,创建 release.yml 文件,通过给分支打上 tag 标签来触发事件,进行工作流程。其它的骚操作就不属于本文讨论范围啦!

基于 JPDA 远程调试 SpringBoot 应用程序

前言

远程调试技术,在开发中相对常见。如果技术团队是采用的基于 DevOps 开发流程,那么你可能需要了解远程调试技术。

在我司,后端开发工程师,几乎不在本地启动服务进行调试。单元测试和接口测试,在某些情况下,并不能很好的复原和追踪哪些诡异的 bug。在我们内部有 2 套环境(开发联调和测试环境),都是在本地开发完,然后将代码推送到“开发环境”服务器,通过 CI/CD 进行自动化部署,然后基于开发环境进行调试,这时,远程调试就派上用场了。

在开发环境完毕后,我们会将代码合并,并推送到测试环境,再用同样的方式部署。这时,测试人员会对应用程序进行测试,试想一下,测试人员在测试出一个 bug 后,通常要汇报一些文本。比如接口地址、请求参数、哪些条件下能够触发 bug、bug 带来的破坏性和影响面等等。然后开发人员再根据这些情况进行场景还原,并对 bug 进行追踪,效率如何暂且不讨论,这里的问题是,你不一定能够复现出来,通常需要走一些复杂的业务流程。这时,利用远程调试,在可疑的地方启动断点,即可一步步追踪。

Java 调试技术 JPDA

Java 平台调试器架构 (JPDA),全称是 Java Platform Debugger Architecture。

  • JVM TI Java Virtual Machine Tool Interface 是一个双路接口,支持 JVM 与本机代理程序之间进行通信。它取代了 JVM DI 和 JVM PI 接口。我们通常在 idea 中打断点让程序停住,那么想在远程也实现,那么 JVM 必须得提供一种代理程序机制,用于统治 JVM 所需的各类信息。JVM TI 支持需要访问 JVM 状态的所有工具,包括但不限于:分析、调试、监控、线程分析和覆盖分析工具。
  • JDWP Java Debug Wire Protocol 是一种通信协议,它是 Java Platform Debugger Architecture 的一部分。它用于调试器和它所调试的 Java 虚拟机之间的通信。它允许在不同的计算机上调试进程。它可以通过网络套接字或共享内存工作。简单点说,想要与 JVM TI Agent 通信,就要先与“通信后端”通信,那么咱们想通信是不是得有一个协议呢?没错,就是 JDWP。
  • JDI Java Debugger Interface 定义了一个高级 Java 语言接口,开发人员可以轻松地使用它来编写远程调试器应用程序工具。JDI 位于 JPDA 的最顶层入口,它的实现是通过 JAVA 语言编写的,所以可以理解为 Java 调试客户端对 JDI 接口的封装调用来启动调试的。

咱们来梳理一下调试的整个流程吧:

IDEA 客户端调试器 -> JDI 客户端 -> JDWP Agent -> JVM TI Agent -> JVM TI -> JVM 应用

更深的原理,就不在这里浪费篇幅了(其实理解起来有点小费劲),咱们关键是得把这玩意用好,才能简化开发,不是么?

在 IDEA 中进行远程调试

先声明一下哈,JPDA 的玩法有很多种,咱们这里只讲最经常用的这一种,也就是在 idea 中调试远程服务器上的 springboot 项目,当然,其它项目也是可以的。

设置应用程序

对于应用程序的运行方式一般没有限制,不过我们通常是将应用程序打包为 jar 包,并使用命令行启动它的,在容器中实际上也是这样。下面俺以 JDK 8 的项目为例,当然,我也会指出不同版本间的差异。

1
java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 remote-debug.jar

注意,这里和我们正常启动应用程序是没有什么区别的,我们只需要加上使用调试代理启动程序的 VM 选项就行了。

  • address 这个参数可能有点歧义,它是配置端口的,但在某些情况下,可能会是 *:5005 或者 0.0.0.0:5005
  • server=y 这个是指定当前应用是作为调试的服务端还是客户端的,默认情况下是 n ,也就是客户端,但是咱们肯定得作为服务端使用,所以得改成 y
  • suspend 这个参数,指定进程是等到调试器连接好了再跑,还是立即启动,通常情况下,为了不影响程序正常运行,我们都会选择 n ,也就是立即启动。
  • timeout 也就是超时时间,单位是毫秒。当 suspend=y 时,该值表示等待连接的超时;当 suspend=n 时,该值表示连接后的使用超时。

设置 IDEA 客户端

我们打开 idea 客户端,找到 Edit Configurations,然后选择 + 新增一个 Remote JVM Debug ,并安装下图中的方式配置。

请注意,ip、端口以及项目模块,记得填自己的哦!如果是在同一台机器上运行,需要填 localhost

在配置完成后,远程应用启动的情况下,我们启动刚才配置的本地应用程序,不出意外的话,你会得到这样一行输出:

Listening for transport dt_socket at address: 5005

连接成功后,尝试在你的 idea 上打个断点调试看看。

不同版本间的区别

你如果看过文档,可能会注意到,不同 JDK 版本,配置会稍有差别。

  • 对于 JDK 1.3 以及更老的版本
-Xnoagent -Djava.compiler=NONE -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
  • 对于 JDK 1.4 版本
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
  • 对于 JDK 1.5 - 1.8 的版本
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
  • 对于 JDK 1.9 及更新版本
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005

注意事项

这里的注意事项,就说一下俺踩过的坑吧!🥲

  • 务必保证远程应用和本地 idea 调试器的代码是一致的,不然会出现许多诡异的现象!
  • module 一定要两边一致,比如咱们一个微服务项目,你在远程启动 service-a,但是本地是 service-b,那肯定不行!
  • 注意开放对应的端口,并且千万注意,Debug 端口和程序端口,是两个不同的端口,不要冲突。
  • 不同应用之间,不要共用一个 Debug 端口,不然会发生什么,俺也没试过~
  • 如果在容器,或者 k8s 部署的话,注意做一下端口映射,比如 Docker 需要加上 -p 5005:5005

最后

说到这里,就差不多啦。我个人认为这还是一个比较方便的技术,虽然任何技术都不是万能的,但是它在合适的场景,确实能帮你解决问题!😊

扩展阅读:Arthas

学习与人生——我的学习方向的改变

以前学习的方向

从毕业之前到工作到现在,这几年的时间,我比较注重自己的工程能力。因为我需要迅速的提升自己的编码能力,来达到企业对于开发者的基本要求。毕竟,我得先保证自己能顺利找到工作,能先吃饱饭,不管是之前在武汉,还是现在的杭州。

我的工作语言是 Java,在之前很长一段时间,我几乎都注重于框架和第三方库的使用。我必须得把工作中会用到的各种技术学会,熟悉他们的使用方法,以及一些基本的原理。比如说我现在工作中用到的技术基本上都是 Spring Boot + Spring Cloud + Spring Cloud Alibaba 全家桶,于是乎我在学习的时候,就是照着官方的【参考文档】来看的。拿 Spring Boot 来举例,基本上照着参考文档,只需要一些时间,就能了解到以下方面的信息:

  • 如何开始使用,或者如何从旧版本升级。
  • 了解到它的核心特点和生态解决方案,比如数据、网络、消息传递、IO等。
  • 任何官方适配的解决方案的【最佳实践】,以及了解到官方是如何推荐每项配置的。

这样不仅能让我少走许多弯路,还能一开始就能了解到比较【标准】的使用方法,也就是【最佳实践】。就拿 Spring Data Jpa 来说吧,因为工作中一直用的是 mybatis,没接触过,索性就抽空自学了。我们可以在参考文档中,找到 Spring Data Jpa 文档的入口,这时就可以根据文档的介绍一步步往下实践了。然后,我们可以去 GitHub 上面找最佳实践,看一看除了官方,其它优秀的社区开发者,是如何使用的。可以从官方仓库的 issues 里面找,尤其是那些提 issues 时,有很详细的 debug 过程的,一般这样的开发者,他的代码质量不会太差,甚至人家的仓库还配备了 wiki 。还可以直接搜 spring data jpa 这个名词,看看哪些仓库用到了,当然,不要忘记了 awesome

如果说,一开始就去百度搜:“如何使用 Spring Data Jpa”,那么你可能会经历一段痛苦的踩坑过程,甚至会给你带来大麻烦。在接触新事物时,一定要改变自己的认知,注重自己的信息来源和质量。感兴趣的话可以看看我之前的一些文章:

👉学习与人生——聊聊我的学习方法 👉学习与人生——利用GitHub学习 👉高效上网与系统性学习

当你学会这些“套路”之后,你能为自己节省许多时间,形成正反馈,从而避免在“内容农场”中迷失自我。

说了这么多,似乎一直在强调我之前如何在学习?但是仔细品味,你就能发现,其中是有一些问题的,至少已经不太适用于现阶段的我了。最开始我说道,我之前过于注重工程能力了,虽说是无奈之举,但到了必须做出改变的时候了。从基础切入,锻炼自己高效的学习能力和问题的解决能力。

改观

当我有了一份稳定的工作、且能独立应付工作中大多数问题的时候,我需要重视自己的学习能力、架构能力和系统设计能力。简而言之,就是努力学习基础知识,系统性的完善自己的知识图谱,并尝试独立设计系统。

计算机基础知识,我想不管再过多少年,可能都不会过时,而某一门技术是有可能的,这一点只要稍微了解下整个计算机技术的发展史就能明白。但是基础只要扎实了,学任何一门技术都会很快。之前也并不是没有这样做,只是时间一方面分配的确实少了,另一方面就是,没有系统性的去学习基础知识,导致部分知识在脑海中比较零散,无法构成体系。而接下来,就是要解决这些问题的时候了。提升了基础和学习能力,才能解决更多的问题,才能一直向上走!

不知道以后有没有机会去一些优秀的公司,去学习他们的工作方法、最佳实践和解决方案,这些东西在书上或者社区是学不到的(很难),所以亲自去体验过,可能会更好。

最后,希望我这个改变是对的,不同的学习方向上,付出和收获都是不一样的,希望能越来越好!😊

我的 Java 日志记录最佳实践

为什么是日志?

对于大多数后端开发来说,日志可能是应用程序给予你反馈的唯一方式。在开发环境,你通常能用 Idea 之类的集成开发工具进行 DeBug 调试,但是在生产环境呢?部分人也许用过远程调试,但生产环境原则上来讲,是不允许你进行任何调试操作的。所以,拥有一份便于查看的日志,对排查问题相当重要。

日志的好处往往在一个系统中是被低估的,甚至很多人根本不重视它。人写日记,是为了记录每一天都干了些什么,而软件的日志,应当如此。作为一名优秀的软件开发工程师,我们应该利用好日志来解决问题,并辅助了解整个系统的健康状况。

如果你对日志还没有系统性的了解,或者不清楚什么是日志,可以参考 Wiki 百科的 日志文件 词条,一步步去了解。

我的最佳实践

以下是我的 Java 日志记录最佳实践。

日志信息的优化

日志虽然能帮助我们排查线上问题,但是日志信息太多的话,也并不是一件好事。所以,结合你自己的系统和业务,对日志记录的繁简程度恰到好处,是一件并不容易的事情。

方法之后

弄清楚这个标题之前,我们得先明确一件事——日志在业务中用来记录已经发生的事情。我来举一个通用的例子吧:

1
2
3
4
5
6
7
// 方法之前打印日志
log.info("准备执行登录操作.[loginParam={}]", loginParam);
userService.login(loginParam);
// 方法之后打印日志
String result = userService.login(loginParam);
log.info("登录执行完成.[result={}]", result);

首先,登录方法,可能是执行了服务间的调用,或者是本地方法。我们从第一个日志是看不出 login() 是否调用成功的,在某些情况下,可能无法产生任何有效的信息。比如说你【没注意到】被调用方抛出的异常、或者说压根就没有做异常处理(相信不少人有过这种经历),那么你这时候就会陷入怀疑之中,根本看不出来是什么情况导致的。

这种情况我是真的碰到过,可能是经验不足导致的,以至于后来专门去了解了日志的相关知识。在这之前,我确实也比较容易忽略它的重要性。

我们再来看第二个日志,当我们看到它的时候,说明它之前的操作是已经成功了。如果说调用失败了,那么你是看不到日志的,应该能直接看到异常。

不同架构下的日志

在传统的单体项目中,我们一般只需对一次请求间经过的所有日志都加上【唯一的 id】—— RequestID,将所有执行过程串联起来。

在 SOA/微服务 等架构下,情况就有所不同了,因为我们需要考虑到服务之间的可跟踪性。比如说我们需要在一个【统一的入口】,使用【唯一的请求标识符】,标记这次请求所执行到的所有的日志。这样才能对日志进行跟踪,虽然每个服务直接打印的日志信息,在海里日志信息里面没有什么特别的关联。但是当我们清洗日志数据之后,可以根据【唯一标识符】,去筛选出所有相关的日志信息。

一般的,我们会用 Elasticsearch 或者 Clickhouse 存储日志,用 Kibana 或者 Grafana 来进行日志分析。这时你会发现,在排查线上问题的过程中,你只要找到对应请求的【唯一标识符】,你就能迅速在海量日志中,找到对应的信息。

分离参数和消息

开发中比较常见的日志类型,主要有两种,一种是咱们自定义的日志消息,另一种是业务参数,当然,可能还有异常信息之类的。下面是一个简单的例子:

1
2
3
4
5
6
7
// 没有分开的情况
restTemplate.getForObject(url, String.class, vars);
log.info("请求路径:{}成功啦!", url);
// 分开的情况
restTemplate.getForObject(url, String.class, vars);
log.info("请求成功啦![url={}]", url);

咋一看上去,2 种日志格式,似乎没多大区别。但实际上,第一种在某些情况下增加了阅读难度,我们不妨想一下,在开发中,是否有碰到过提示消息很长的日志语句?或者是业务参数特别的长,这种情况下将业务参数放在末尾是否更好呢?而且如果要添加新参数的话,重写起来相对而言也更麻烦不是吗?当然,第一种可能从某些角度来说,写起来会爽一些。

如果说你接触过 LogStash 的话,那么你应该了解 Grok 插件,第一种写法,将会导致Grok 模式的解析变得更加困难,而第二种写法,就不存在这种情况,因为语法很清晰。

日志格式的优化

拿 SpringBoot 项目来举例说明,一般我们会在对应的 xml 文件中配置日志的输出格式。这些文件可能是 logback-spring.xmllog4j2.xml 等。

日志格式结构化

一般来说,日志输出文件中,主要会包含如下内容:

  • 日志打印时间
  • 日志输出级别
  • 自定义的【唯一标识】
  • 线程名称
  • 调用链标识
  • 日志内容
  • 程序异常堆栈
pattern="[%d{yyyy-MM-dd HH:mm:ss}] %-5level {requestId=%X{requestId}} %class{36} %L %M - %msg%xEx%n"

我们可以看到这个例子,开头是日志时间、其次是日志级别、然后是 requestId ,消息放在了最后。实际上格式并没有太多特殊的要求,更多的是你需要结合项目自身情况,和你的团队成员共同制定应该用什么格式。

日志级别控制

日志级别有 8 种,去掉全部显示和全部关闭,还剩如下 6 个级别:

  • TRACE — TRACE 级别指定比 DEBUG 更细粒度的信息事件,因为级别很低,所以一般用不到。
  • DEBUG — DEBUG 级别指定对调试应用程序最有用的细粒度信息事件,一般用于开发过程中打印运行(技术)信息。
  • INFO — INFO 级别指定信息消息,在粗粒度级别突出显示应用程序的进度,一般用于打印业务信息。
  • WARN — WARN 级别表示潜在的有害情况,一些特定的错误信息,也会是一些提示。
  • ERROR — ERROR 级别指定可能仍允许应用程序继续运行的错误事件,一般打印错误和异常信息。
  • FATAL — FATAL 级别指定可能导致应用程序中止的非常严重的错误事件。

一般来说,DEBUG 和 INFO、WARN 和 ERROR 容易混淆。在一个开发团队中,通常日志级别的设定标准是需要达成一致的,可以参考 [The Syslog Protocol](The Syslog Protocol)。最好是对不同级别的日志,定义不同的输出格式,而不是由开发人员自由发挥。

日志性能优化

我们要确保在打印日志时不会增加额外的系统响应时间,毕竟有些地方的日志是必须的。尽可能以异步方式写入日志,可以先写入到本地日志文件,再使用日志转发器发送给相应的服务,也可以使用消息队列,但这种方式可能会存在延迟,不过相对于对业务性能的影响而言,都是可以接受的。

日志输出文件

我们需要将不同的日志输出到不同的文件,因为不是很多小伙伴参与的项目,都是使用了诸如 ELK 之类的东西。这时候为了方便对线上错误进行分析,如果所有的错误日志同时被输出到了一个单独的文件之中,那么排查起来将会省事很多。

一些场景下的使用规范

  • 使用标准的日志对象,而不是直接创建日志实现类。
1
2
3
4
5
// 在类上面加上注解
@Slf4j
// 或者直接使用下面这行代码创建
private static final Logger log = LoggerFactory.getLogger(Main.class);

面向接口编程,而不是面向实现,这是软件设计模式之一。上面 2 种方式,实现的效果是一样的(编译后再看你就明白了),在项目中统一使用某一种即可!

  • 不推荐标准输出
1
2
3
4
5
6
7
try {
// todo
} catch (Exception ex) {
Syste.out.println(ex.getMessage()); // 不推荐
 System.err.println(ex.getMessage()); // 不推荐
 log.error("error message", ex); // 推荐
}
  • 不推荐 printStacktrace
1
2
3
4
5
6
try {
// todo
} catch (Exception ex) {
ex.printStacktrace(); // 不推荐
 log.error("error message", ex); // 推荐
}

这个 printStacktrace() 几乎所有人都用过,不知道大家有没有点进去看过一眼源码,实际上跟上面那一条说的道理是一样的。

1
2
3
public void printStackTrace() {
printStackTrace(System.err);
}
  • 注意不要输出掉了关键信息
1
2
3
4
5
6
7
try {
// todo
} catch (Exception ex) {
log.error(ex.getMessage()); // 不推荐
 log.error("error message", ex.getMessage()); // 不推荐
 log.error("error message", ex); // 推荐
}

这种 ex.getMessage() 写法,我在不少项目见过,曾经我也经常用,但随着学习的深入,发现这样做其实并不太好。因为这样的日志输出方式,往往会丢失掉最重要的 StackTrace 信息。

  • 日志异常二选一
1
2
3
4
5
6
try {
// todo
} catch (Exception ex) {
log.error("error message", ex); // 两者冲突
 throw new RuntimeException(ex); // 推荐
}

如果捕获异常后,因为业务需求,需要再次往外抛异常,那么可以不用写上面那句错误日志。因为这种情况下,异常都是交由最终捕获方去进行了处理的,很容易造成重复日志输出。你想一想,全局异常处理的地方,是不是会单独打印一条错误日志?当然,还得看项目本身的实际情况,这里只是推荐二选一。

  • 别 TM 在循环中打印日志了 🥲
1
2
3
for (int i=0; i < vars.size(); i++) {
log.info("不要在循环中打日志!!!");
}

注意上面这个循环,如果循环次数很多,想象一下,是不是会产生很多无意义的日志?而且可能会影响程序的性能!

最后

想玩转日志,要注意的点有很多,日志容易被人们忽略,但它也很重要。日志跟踪、日志聚合、日志指标等等,都有不少知识点。这篇文章汇集了我自 20 年毕业到现在以来,对于日志系统而言踩过的坑,以及一些学习的总结。当然,要学的还有很多,不同团队、不同项目采用的方案也不同。但是你对日志系统有一个全面的认识之后,不管面对哪一种情况,你都能游刃有余。哪怕是参与到团队日志规范的制定,你也不会听的云里雾里,甚至可以提出你自己的观点。全面认知过日志系统后,你也能大幅度减轻你和同事对线上问题排查的负担。

当然,还有更多关于日志的内容,我这里没有写,如果你想了解,请善用搜索引擎。

扩展阅读:美团技术团队:如何优雅地记录操作日志?

普罗米修斯の初体験

前言

为什么我会选择普罗米修斯(Prometheus)?Prometheus 是按照 Google SRE 运维之道的理念构建的,具有实用性和前瞻性。同时也是基于 Go 语言开发的,性能好,安装部署也简单,甚至跨平台(包括 arm 平台)。作为对服务基础和业务监控,Prometheus 是一个非常好的选择。

什么是普罗米修斯?

咱们这里引用官方的术语: Prometheus 是一个开源系统监控和警报工具包,它可以收集系统信息,并将其发送到一个或多个监控中心。Prometheus 将其指标收集并存储为时间序列数据,即指标信息与记录它的时间戳一起存储,以及称为标签的可选键值对。

这里引入官网的图,说明 Prometheus 的架构及其一些生态系统组件:

安装

安装方式有很多,二进制包或者 Docker 都可以,这里我们选择二进制包。

安装环境

这里俺用的是 Ubuntu 20.04,别问我为啥,主要是我内存最大的机器就是这台了(24G 内存),只不过,它是 arm64 架构的,所以下面的教程是运行在 arm64 架构的服务器上面的,当然,你也可以用本教程在 amd64 架构下安装,只有一些细微的区别,咱们下面会讲到。

安装 Prometheus

下载 Prometheus

你可以去官网,或者 GitHub 的发布页面下载安装包,这里我下载的是 GitHub 仓库的包。

  • 如果你是 amd64 架构的服务器
1
2
3
4
wget https://github.com/prometheus/prometheus/releases/download/v2.31.0/prometheus-2.31.0.linux-amd64.tar.gz
tar xfz prometheus-2.31.0.linux-amd64.tar.gz
sudo cp prometheus-2.31.0.linux-amd64/prometheus /usr/local/bin/
sudo cp prometheus-2.31.0.linux-amd64/promtool /usr/local/bin/
  • 如果你是 arm64 架构的服务器
1
2
3
4
wget https://github.com/prometheus/prometheus/releases/download/v2.31.0/prometheus-2.31.0.linux-arm64.tar.gz
tar xfz prometheus-2.31.0.linux-arm64.tar.gz
sudo cp prometheus-2.31.0.linux-arm64/prometheus /usr/local/bin/
sudo cp prometheus-2.31.0.linux-arm64/promtool /usr/local/bin/

最主要的差异,就是要下载不同的包,后面的配置几乎相同

配置 Prometheus

  • 检查
1
prometheus --version

执行命令,出现如下图所示,就成功了!

我们在刚才解压后的文件夹下面,可以找到一个子目录 prometheus ,然后可以找到一个配置文件 prometheus.yml 。咱们现在需要把 prometheus.yml 这个初始配置文件复制到 /etc/prometheus 目录下,然后简单配置就可以启动啦。当然,你也可以按照自己的需求来配置,具体可以参考官方的配置文档

1
2
sudo mkdir -p /etc/prometheus
sudo cp prometheus.yml /etc/prometheus/
  • 默认的部分配置如下:
1
2
3
4
scrape_configs:
 - job_name: "prometheus"
 static_configs:
 - targets: ["localhost:9090"]

我们可以看到端口是 9090 ,你可以按需求改为其它的。

启动 Prometheus

接下来咱们启动看看

1
prometheus --config.file "/etc/prometheus/prometheus.yml"
  • 如果发生了异常,则可以使用 prometool 工具来检查你的配置文件。
1
promtool check config "/etc/prometheus/prometheus.yml"

如果输出如下提示,说明没问题了!

Checking /etc/prometheus/prometheus.yml
SUCCESS: 0 rule files found

配置 service 方式运行 Prometheus

  • 新建一个 service 文件
1
sudo vim /etc/systemd/system/prometheus.service
  • 编辑如下内容保存即可!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[Unit]
Description=prometheus
[Service]
User=root
ExecStart=prometheus --config.file "/etc/prometheus/prometheus.yml"
Restart=on-abort
[Install]
WantedBy=multi-user.target
  • 设置开机启动
1
sudo systemctl enable prometheus
  • 启动 Prometheus
1
sudo systemctl start prometheus

这样便大功告成啦!

配置 HTTPS 和反向代理

如果你的服务器是 HTTPS 的,那么需要配置 HTTPS 的证书和私钥,这里俺使用的是 Let's Encrypt 的证书,可以去官网下载。具体如何操作就不说了,如果你不会,应该去学习如何使用 Nginx,并学会配置 HTTPS 证书。

  • 配置 Nginx 反向代理

这里我放出我的配置吧,你可以根据你的需求,参考使用:

location /
{
proxy_pass http://127.0.0.1:9090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
add_header X-Cache $upstream_cache_status;
#Set Nginx Cache
add_header Cache-Control no-cache;
}

Prometheus 启动监听在本地回环地址 localhost:9090 ,所以公网是无法直接访问的,我也不太建议你开放防火墙,这样能带来一定的安全保护。

预览

这时候咱们访问域名,就能看到页面啦!

这里提示,我这不是生产环境,只是拿来练手学习,所以无所谓,生产环境不建议这么做!可以通过 ssh 端口转发方式实现远程访问。

学习与人生——使用 GitHub Copilot 后的思考

起因

在 10 月 27 号的时候,我收到了一封邮件,是一封关于 GitHub Copilot Preview 的测试邀请邮件。当然,这是我提前申请过的,怀着无比激动的心情,体验了一番。三分钟的热度过去之后,随之而来的担忧,不得不让我认真的反思这项技术所带来的问题。

GitHub Copilot

什么是 GitHub Copilot ?

这里引用官方的术语:GitHub Copilot is an AI pair programmer that helps you write code faster and with less work. GitHub Copilot draws context from comments and code, and suggests individual lines and whole functions instantly.

大体意思是:GitHub Copilot 是一个“搭档”程序员,可帮助您以减少工作量和更快地编写代码。GitHub Copilot 从评论和代码中提取上下文(即当前文件内容和光标位置),并立即给出单行或整个函数的代码建议。

GitHub Copilot 可以做什么?

将注释转换为代码

比如说,我在文件中写了一行注释:【新增商品套餐】,然后我就可以在 GitHub Copilot 中看到一个建议:

此时我按下了【Tab】键,GitHub Copilot 就会自动将注释转换为代码,并且自动插入到当前光标位置。甚至 Github Copilot 提示的方法,并不存在我的代码中,而是自动帮我生成的。

自动填充重复代码

我在图中举例一个场景,如果我想在一个方法中完成一系列的事情,但是其中一项是需要把用户信息转换成其它 Dto 对象,而恰好我在另一个地方这么用过的话。此时我只需要写下注释即可:

然后我就可以在 GitHub Copilot 中看到一个建议,并且我主观上认为这个建议是按照我的需求来的,那我便可以按下【Tab】键,GitHub Copilot 就会自动将注释转换为代码,并且自动插入到当前光标位置。

换做以前,我写这段代码的时候,我估计会对代码进行抽样封装,然后再把这段代码放到一个方法中。或者直接找到相应的代码进行复制粘贴。 当然,无论哪种方式,我们自己的处理肯定跟 Github Copilot “不太一样”,要结合上下文和实际情况判断,哪种选择才是最优的。Github Copilot 能给出解,但不一定是最优解。

其它

除此之外,GitHub Copilot 还能自动填写测试方法,甚至还能直接通过它获得一些【隐私信息】,不知道这个问题后面会不会解决。 顺便说一下快捷键,感兴趣的小伙伴们可以去尝试: 在 Windows 或 Linux 上,按 Alt + ] 下一个或 Alt + [ 上一个。 接受建议,按下 Tab 键,就能帮你自动填充了,按下 Esc 就会取消建议。

GitHub Copilot 是如何工作的呢?

根据官方的说明,主要的训练集来源于 GitHub 上的开源仓库的代码(私有仓库有没有用俺就不清楚了),以及自然语言训练。 在你写代码的过程中,GitHub Copilot 会将你的代码上传至服务器,然后 AI 会根据接收到的上下文进行处理,你用的越多,模型的输出就越精确。

误解

为什么会说误解?

很多人肯定跟我一样,对 GitHub Copilot 会产生偏见和误解,认为这不过是“人”写出来的“人工智能”,通过训练大量现有的代码,从而对目标代码进行运算后,给出模板代码而已。 我最开始看到 GitHub Copilot 介绍时,确实是这么想的,但是当我自己体验过后,看法就发生了改变。但也可能是它足够的强大,致使(欺骗)我的想法改变了。 在往后阅读之前,我希望你明白一件事儿:软件开发的主体仍是你自己,并不是 GitHub Copilot,它无法也难以取代你!

我总结了一下一些可能产生的误解,同时也是我暂时还没弄明白的事情(没证据):

  • GitHub Copilot 只是跟官方说的一样,依靠大量现有的训练集和自然语言训练而来。

这里是指,它只能通过已经存在过的代码给你提示,并不能自己想出全新的代码。这点我不敢肯定,但是我认为它迟早能做到。我在使用过程中,它确实能给我提示在我的程序中未出现过的代码,但是没有证据表明其它人没写过。

与其他训练目标是预测下一个词符的大型语言模型一样,Codex 会生成与其训练分布尽可能相似的代码。这样做的一个后果是,这种模型可能会做一些对用户无益的事情。

  • GitHub Copilot 不擅长“编程”,没有“编程思想”

可能真的没有,但是“编程思想”毕竟难以被量化,但是它从某方面来说,是擅长编程的。

  • GitHub Copilot 缺乏创造性

对它自己而言,我不敢说。但是对我而言,可以说是充满了创造性。GitHub Copilot 有时候提示的代码,会让我思考,如何写才是比较好的方案,怎么写才更优雅?

  • GitHub Copilot 的能力不强,“感觉没啥用”

咱不能忘了,这玩意是 AI,时间越长,他只会被训练的越来越聪明。

不知道大家还记不记得 Google 围棋 AI AlphaGo? 这里咱们引用国际围棋联盟秘书长李夏辰的一段话吧:

第一次听说电脑要挑战顶级棋手李世乭时,我非常惊讶。 我觉得挑战者一定对顶级棋手有多强毫无概念; 但事实上,是我对电脑有多强没有概念。

担忧

GitHub Copilot 会取代我吗?

我个人是对此持乐观态度的,因为我觉得它的最终的设计目标一定是最大限度的提升【开发者效率】,而不是很多人担心的【替代开发者】,至少短时间内不会。

GitHub Copilot 无法解决系统复杂度的问题,咱们编程最核心的,并不是你多会用 GitHub Copilot,也不是你会用多少框架,最核心的是人,那个坐在电脑面前敲代码的人。作为一个优秀的开发者,你必须掌握哪些基础知识,而不是陷入框架、插件和工具的使用当中。

最重要的,AI 无法理解来自产品经理和客户的需求,也无法沟通。

GitHub Copilot 安全吗?

咱们可以肯定的一点是,你如果要使用它,那么你的代码以及配置文件,都是会被传送到 GitHub Copilot 的服务器的。这也就意味着你的隐私、配置文件、访问密钥等等,都会有泄露的风险。

如果你在政府或者银行工作,那么请一定不要在工作时使用它,因为风险极大,你必须对此负责!如果你是在普通的公司上班,也请三思而后行!如果你觉得 GitHub Copilot 会给你们公司的开发带来帮助,至少得拉上部门相关负责人和项目组开一个评审会议后再做决定!

总结和一些建议

大家应该都听说过“结对编程”(Pair Programming)吧?这是一种敏捷软件开发的方法,两个程序员在一个计算机上共同完成工作。一个人输入代码,另一个人审查他输入的代码。这是不是和你使用 GitHub Copilot 时很像?但是你却不能和 GitHub Copilot 互换角色,甚至不能和它沟通。

AI 自动生成程序,是根本不可能的事情。就像我上面说的,AI 无法理解来自产品经理和客户的需求,也无法沟通。让 AI 凭空生成一个程序,是没有意义的,所以你必须告诉 AI,你想要什么。而最难的,恰恰就是你“表达需求”的这个过程,难度甚至不亚于编程。咱们好好想一想,编程的本质,是不是在告诉电脑自己想要什么?输入什么就必然输出什么,数据结构、算法等等,已经被 AI 拿去训练了,但是表达“想要什么”,它自己是无法自动完成的。没有任何机器可以代替人的思考,所以俺觉得,程序员是一种无法被机器取代的工作。

你的雇主可能不会这么想,他们可能会认为,好的工具节省了你的编程开销,甚至会成为压你工资的借口之一。他们会把你当成机器上的“可替换零件”,觉得随时可以替代掉。其实这样往往会适得其反,节省人力开销最好的办法,就是聘用优秀的程序员,给他们足够的尊重,良好的待遇、让他们开心的工作和生活,就能让他们做出更好地软件。那些名词满天飞,光说不做的管理者,才是真正浪费公司资源的人,降低软件开发效率和软件质量的祸根。

最好的发展方向是什么?我觉得人的【智慧】+ 机器的【智能】相结合,才应该是未来人工智能发展的方向。

最后给列举一些建议吧:

  • 使用 AI 辅助编程时,注意隐私和法律风险,切记不要随意在公司使用。
  • 不要过度依赖 GitHub Copilot 之类的 AI,编程的主体和核心应该是你自己,也不要因为用着爽就忘记了自己应该如何学习了。
  • 如果你担心被 AI 取代(卷起来了🤣),那么你就应该去面对它,去学习编程知识是你最重要的事情之一。
  • 兴趣不是最好的老师,生存才是!

在微服务项目中引入 knife4j

什么是 Knife4j ?

knife4j 是为 Java MVC 框架集成 Swagger 生成 Api 文档的增强解决方案。说白了,如果项目开发为前后端分离开发的话,这个插件就非常的省事儿,不用再很麻烦的写接口文档了。之前用过 Swagger 来生成文档,但是在某些情况下,Swagger 却并不适合国内的项目,尤其是验收文档。给甲方的验收文档往往包含接口文档,这是 knife4j 的导出就派上用场了。

没有好不好用,只有适不适合!

引入项目

说明

本文章以我自己的开源项目 aurora-mall 为例,详细讲述在 Spring Cloud 2020 & Alibaba 2021 中,应该如何引用。为什么必须加上这个说明呢?因为不同的项目,引入方式还是有一些差别的。

开始

导包

  • 在项目的根 pom.xml 文件中导包。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<properties>
<knife4j.version>2.0.4</knife4j.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-micro-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
  • 在网关服务的 pom.xml 中,导包
1
2
3
4
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
  • 在每一个需要生成文档的服务的 pom.xml 中,导包(一般放在 common 中,然后不需要屏蔽即可)
1
2
3
4
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-micro-spring-boot-starter</artifactId>
</dependency>

gateway 服务模块的接口,需要多一个包,用来输出文档到前端(包含UI包)它会把我们所有的微服务都聚合到一个文档,统一输出到前端,其它服务只需控制数据,这样便于节约资源!

生产环境屏蔽

目前 Springfox-Swagger 以及 Knife4j 提供的资源接口包括如下:如果你要用的话,记得白名单放行

资源 说明
/doc.html Knife4j提供的文档访问地址
/v2/api-docs-ext Knife4j提供的增强接口地址,自2.0.6 版本后删除
/swagger-resources Springfox-Swagger提供的分组接口
/v2/api-docs Springfox-Swagger提供的分组实例详情接口
/swagger-ui.html Springfox-Swagger提供的文档访问地址
/swagger-resources/configuration/ui Springfox-Swagger提供
/swagger-resources/configuration/security Springfox-Swagger提供

当我们部署系统到生产系统,为了接口安全,需要屏蔽所有 Swagger 的相关资源

如果使用 SpringBoot 框架,只需在 application.properties 或者 application.yml 配置文件中配置

1
2
3
4
5
knife4j:
 # 开启增强配置 
 enable: true
 # 开启生产环境屏蔽
 production: true

Java 化配置

  • 配置基类

咱们一般会放在 common 包里面。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package com.besscroft.aurora.mall.common.config;
import com.besscroft.aurora.mall.common.domain.SwaggerProperties;
import org.springframework.context.annotation.Bean;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import java.util.ArrayList;
import java.util.List;
/**
 * swagger 通用配置
 *
 * @Author Bess Croft
 * @Date 2021/4/2 12:28
 */
public abstract class BaseSwaggerConfig {
@Bean
public Docket createRestApi() {
SwaggerProperties swaggerProperties = swaggerProperties();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo(swaggerProperties))
.select()
.apis(RequestHandlerSelectors.basePackage(swaggerProperties.getApiBasePackage()))
.paths(PathSelectors.any())
.build();
if (swaggerProperties.isEnableSecurity()) {
docket.securitySchemes(securitySchemes()).securityContexts(securityContexts());
}
return docket;
}
private ApiInfo apiInfo(SwaggerProperties swaggerProperties) {
return new ApiInfoBuilder()
.title(swaggerProperties.getTitle())
.description(swaggerProperties.getDescription())
.contact(new Contact(swaggerProperties.getContactName(), swaggerProperties.getContactUrl(), swaggerProperties.getContactEmail()))
.version(swaggerProperties.getVersion())
.build();
}
private List<ApiKey> securitySchemes() {
// 设置请求头信息
 List<ApiKey> result = new ArrayList<>();
ApiKey apiKey = new ApiKey("Authorization", "Authorization", "header");
result.add(apiKey);
return result;
}
private List<SecurityContext> securityContexts() {
// 设置需要登录认证的路径
 List<SecurityContext> result = new ArrayList<>();
result.add(getContextByPath("/*/.*"));
return result;
}
private SecurityContext getContextByPath(String pathRegex) {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex(pathRegex))
.build();
}
private List<SecurityReference> defaultAuth() {
List<SecurityReference> result = new ArrayList<>();
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
result.add(new SecurityReference("Authorization", authorizationScopes));
return result;
}
/**
 * 自定义Swagger配置
 */
public abstract SwaggerProperties swaggerProperties();
}
  • 新建 SwaggerProperties 类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Data
@Builder
@EqualsAndHashCode(callSuper = false)
public class SwaggerProperties {
/**
 * API文档生成基础路径
 */
private String apiBasePackage;
/**
 * 是否要启用登录认证
 */
private boolean enableSecurity;
/**
 * 文档标题
 */
private String title;
/**
 * 文档描述
 */
private String description;
/**
 * 文档版本
 */
private String version;
/**
 * 文档联系人姓名
 */
private String contactName;
/**
 * 文档联系人网址
 */
private String contactUrl;
/**
 * 文档联系人邮箱
 */
private String contactEmail;
/**
 * 版权信息
 */
private String license;
/**
 * 版权协议地址
 */
private String licenseUrl;
}
  • 在网关服务新增 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.besscroft.aurora.mall.gateway.config;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.util.ArrayList;
import java.util.List;
/**
 * swagger资源配置
 *
 * @Author Bess Croft
 * @Date 2021/4/2 12:37
 */
@Slf4j
@Component
@Primary
@AllArgsConstructor
public class SwaggerResourceConfig implements SwaggerResourcesProvider {
private final RouteLocator routeLocator;
private final GatewayProperties gatewayProperties;
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routes = new ArrayList<>();
//获取所有路由的ID
 routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
//过滤出配置文件中定义的路由->过滤出Path Route Predicate->根据路径拼接成api-docs路径->生成SwaggerResource
 gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId())).forEach(route -> {
route.getPredicates().stream()
.filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
.forEach(predicateDefinition -> resources.add(swaggerResource(route.getId(),
predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
.replace("**", "v2/api-docs"))));
});
return resources;
}
private SwaggerResource swaggerResource(String name, String location) {
log.info("name:{},location:{}", name, location);
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("2.0");
return swaggerResource;
}
}
  • 配置网关的 Swagger 处理器,自定义 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.besscroft.aurora.mall.gateway.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.*;
import java.util.Optional;
/**
 * 自定义Swagger的各个配置节点
 *
 * @Author Bess Croft
 * @Date 2021/3/30 21:35
 */
@RestController
public class SwaggerHandler {
@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Autowired(required = false)
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;
@Autowired
public SwaggerHandler(SwaggerResourcesProvider swaggerResources) {
this.swaggerResources = swaggerResources;
}
/**
 * Swagger安全配置,支持oauth和apiKey设置
 */
@GetMapping("/swagger-resources/configuration/security")
public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}
/**
 * Swagger UI配置
 */
@GetMapping("/swagger-resources/configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}
/**
 * Swagger资源配置,微服务中的各个服务的api-docs信息
 */
@GetMapping("/swagger-resources")
public Mono<ResponseEntity> swaggerResources() {
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
}
  • 在每个业务系统服务配置 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
25
26
27
28
29
30
31
32
33
34
35
36
package com.besscroft.aurora.mall.admin.config;
import com.besscroft.aurora.mall.common.config.BaseSwaggerConfig;
import com.besscroft.aurora.mall.common.domain.SwaggerProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
 * swagger配置
 *
 * @Author Bess Croft
 * @Date 2021/3/30 13:43
 */
@Configuration
@EnableSwagger2
@Profile(value = {"dev"})
public class SwaggerConfiguration extends BaseSwaggerConfig {
@Override
public SwaggerProperties swaggerProperties() {
return SwaggerProperties.builder()
.apiBasePackage("com.besscroft.aurora.mall.admin.controller")
.title("极光商城开发文档")
.description("后台相关接口文档")
.contactName("Bess Croft")
.contactEmail("besscroft@foxmail.com")
.contactUrl("https://github.com/besscroft/aurora-mall")
.license("Open Source")
.licenseUrl("https://github.com/besscroft/aurora-mall/blob/main/LICENSE")
.version("0.1.0")
.enableSecurity(true)
.build();
}
}

这里注意2点:

  • @Profile 注解可以规定在哪个环境下生效,咱们只控制开发环境就ok了。
  • apiBasePackage 设置生成的接口在哪个包里面。

同时,如果网关配置了白名单机制,记得放行 /v2/api-docs 地址!因为这里咱没做认证功能!(对于线上生产环境来说,白名单可以更好地控制访问权限,能一定程度保证安全性!)

启动

然后咱们启动项目,来查看是否配置成功!

访问分组接口地址:

1
http://localhost:8000/swagger-resources
[
{
"name": "mall-auth",
"url": "/mall-auth/v2/api-docs",
"swaggerVersion": "2.0",
"location": "/mall-auth/v2/api-docs"
},
{
"name": "mall-admin",
"url": "/mall-admin/v2/api-docs",
"swaggerVersion": "2.0",
"location": "/mall-admin/v2/api-docs"
},
{
"name": "mall-elasticsearch",
"url": "/mall-elasticsearch/v2/api-docs",
"swaggerVersion": "2.0",
"location": "/mall-elasticsearch/v2/api-docs"
},
{
"name": "mall-log",
"url": "/mall-log/v2/api-docs",
"swaggerVersion": "2.0",
"location": "/mall-log/v2/api-docs"
}
]

有正常值,表示接口返回正常!

  • 访问前端地址:
1
http://localhost:8000/doc.html#/home

注意,实例中的 8000,指的是网关的端口号!

使用注解

实际上就是 Swagger 的注解,比如说:

  • 接口类上面可以加 @Api(tags = "主页接口")

  • 接口方法上可以加 @ApiOperation(value = "首页排行接口")

  • 实体类上可以加 @ApiModel(value = "登录对象")

  • 实体类字段上可以加 @ApiModelProperty(value = "用户名", dataType = "String")

  • 当然,不加也可以默认扫描读取!

白名单

最后,放上一些可能需要放行的白名单地址:

1
2
3
4
5
6
7
"/doc.html"
"/v2/api-docs-ext"
"/swagger-resources"
"/v2/api-docs"
"/swagger-ui.html"
"/swagger-resources/configuration/ui"
"/swagger-resources/configuration/security"

最后

我觉得 knife4j 最大的好处就是,可以导出离线文档,有:Markdown、Word、Html、OpenAPI 格式!如果它正好命中你的需求,不妨试试引入到项目中呢!

学习与人生——聊聊我的学习方法

2 次“网络视频研讨会”

在 8 月份的时候,当时的工作还不算特别的忙,于是便组织了 2 次“网络视频研讨会”。当时的初衷就是抱着与他人能够在网上面对面的交流和思想碰撞。

所以,我以问卷调查的方式,汇集了一些有想法的朋友,在 2 个美好的周末,组织了 2 次。至于为什么没有第 3 场了,一方面是因为我的工作越来越忙,没有时间准备且需要休息;另一方面则是,参与的人数还是太少,多少有些提不起劲来了。这种形式并不像提笔写字,把想法写下来就行。当你倾诉时,没有人倾听,实际上是难以坚持下去的!

第一次交流时,可能是图个新鲜,大家交流都比较积极。自我介绍、闲聊环节、分享近期状况以及对未来规划的交流,基本上是大家都发言。而扩展话题,主要就是由我来主讲。第一期主要的环节——互相认识、围绕每个人自身进行总结、个人规划、以及对后续会议有无建议。

第二次交流,实际上新增了一个流程——嘉宾发言。主要是让第一次之后参与进来的同学,进行发言,当然,你想分享也是可以的。第二期主要的环节——嘉宾发言、系统性学习(在任何环境下如何学习)、作为一名开发人员你需要完善自己的基础设施。

我平常是如何学习的?

学习前期的准备

目标规划

不知道是否已经养成了习惯,还是怕自己经常会迷茫,不知道该干啥。以至于我经常会给自己指定目标和规划。常用的软件比如:Trello、Notion、JIRA等。

上大学那会儿,还只会用 Trello,写文章时打开了以前的看板,截了个图。

毕业之后,就开始以 Notion 为主了,相比而言,功能更强大,但是也有一些不足。

回到正题,做目标规划的意义是什么呢?

  • 清空大脑,把大脑从「要做什么」和「怎么做」的混乱当中拯救出来。
  • 随时随地都明确地知道,下一步应该做什么。

估计可能有读者朋友已经发现了,这2句话,其实是 GTD 的目的。GTD(Getting Things Done) 时间管理法则,我的理解就是,把事情处理完!这里俺就不详细说 GTD 了,因为我目前也在实践阶段,等自己“完全掌握”或者有所心得了,再跟大伙儿聊聊!

学习如何正确的开始?

如果你要开始学习一个新东西,却不知道如何正确的开始,那么不妨参考下我的方法——“从思维导图开始,从思维导图结束。”

拿 Golang 学习为例吧,在我在最开始学习前,肯定不能再像我高中刚入门 Java 时那样买本书就开始盲目地学习了。我先去网上找到了 Golang 学习路径图,这个对我学习的方向有极大的帮助。

👉Go 开发者路线图

然后,我自己又讲里面的内容,整理到了自己的思维导图里面。

然后,根据路线图来学(学习不要一步登天、也不要心浮气躁),我怕自己学的太枯燥,又不想在找书上面浪费太多的时间,于是就选择了——《Head First Go语言程序设计》。这真的是一本极佳的入门书,里面在恰当的位置,总是能出现有趣的插图。想当初我在高中买的第一本书时,体验就远不如这本。当时冲着名字买——《Java 从入门到精通》,看上去很震撼,实则不然,前往精通的路上,竟然是我一步步用笔注解书中的错误过来的。直到高二时,买了一本 Oracle 官方的《Java 8 编程入门官方教程》,才对 Java 这门语言有了不一样的了解,后来才一步步纠正了学习的方法,逐渐熟练起来。

学习动机

我们都知道,学习可以分为两类:主动学习和被动学习。对于我来说,上学时,我喜欢的科目,叫主动学习;我不喜欢的科目,叫被动学习(这可能也是我偏科的原因之一吧)。而现在,学习编程,就完完全全的是主动学习了。

找到自己的兴趣点

为什么我学习编程属于主动学习呢?因为编程属于我的兴趣点。也就是咱们常说的,对某种事物感兴趣。每当我开发出一个新东西、解决一个新 bug、学习一个新的技术点,都会让我获得满足感与成就感。

这里其实我父亲的作用也挺大的,在我小时候,只是对计算机和编程“充满好奇”,算不上感兴趣。小孩子好奇的时候,总喜欢问各种奇怪的问题。但是我父亲有意的“引导”我,强化了对计算机和编程的好奇心。也许是为了让我少打游戏这么干的吧,我也没有找他本人确认过,但是有一点可以肯定的是,不善的引导、甚至用小孩子认为的更粗暴的方式对对待他这种行为的话,很容易讲好奇心扼杀在摇篮中的!

成隐性和快感

不知道大家有没有这样的感受,就拿我自己来说吧。当我在家“折腾”项目时,或者写开源项目时,总是乐在其中(当然,音乐也是得放的)。在这个过程中,就会让人产生快感——过程性快感。而当我 commit 代码时,也会感到快乐——结果性快感。当你经常性的出现这些快感的话,就会产生“成隐性”。以至于只要周末一有空,我就会写写代码。

我想,玩游戏可能也是这样吧,一玩就停不下来。但如果你 LOL 跟人一起五黑,然后连输10把,大概率你最近都不想打了。🤣

学习媒介很重要

我最常用哪些媒介学习?

我在大学时,不像现在这么喜欢看书自学,而是更倾向与看视频学习。但是却从来没有一个视频是看完了的,往往看了几个视频后,就看不进去了。最重要的一点,不知道大家有没有体会过,看完之后,你感觉自己什么都没学会,是不是?以至于后来,我学习编程就剩下3种主要方式:

  • 看书
  • 看官方文档
  • 看别人的开源项目

为什么我更喜欢看书而非视频?

一句话总结:看视频会导致你的思考变少,而看书不会!

看视频学习的时候,往往难以深入思考。而视频本身的质量,我们是不确定的,更别说为了吸引流量而特地设计的视频了。如果你想看质量更高的视频,我推荐你去 YouTube 看公开课,当然,有时候你也能在 B站找到中译版的公开课视频。

从时间的角度来讲,更不应该看视频。一句话,用眼睛看完的时间,肯定是会小于用嘴巴念完的时间。并且,视频是有很多“空白时间”的,空白时间你真的用来思考了吗?别人一个方法,敲了多久你就要等多久再看,但是文字版你能直接阅读。而且视频版切来切去的,都容易打断思维。所以,我个人是更青睐与看书的,看书能够让我更多地独立思考!

学习结果的产生

咱们不能光学不练呀,是吧?在看编程书的时候,一定要照着书上的代码示例,手动敲一敲。你可以预先在脑海中演练一遍,然后再去手动敲出来,看是否符合你的预期。炉火纯青之后,再省略这一步,节约时间。

如果是学习新的知识,一定要做好笔记和总结,同时要画思维导图,这样你才能把知识消化掉!

高效上网与系统性学习

前言

我接触互联网的十几年,整个世界似乎发生了天翻地覆的变化。过去的人们,喜欢贴吧,论坛。而现在,不知道为什么,就拿起了手机,仿佛离手之后就好像失去了什么。打开抖音或者B乎,一个接一个的滑动,能够看到大半夜不睡觉。

手里握着手机时,我们好像变成了幽灵,在现实和虚拟两个世界间摇摆。——《玻璃笼子》尼古拉斯·卡尔

英文搜索

为什么我们需要英文搜索?

我不知道大家有没有跟我一样的经历。比如说在程序开发过程中,遇到问题,我可能会去搜索引擎搜索。但是我们必须明白的事情是,中文互联网的内容农场实在是太多了。几乎全是采集站,不仅对你毫无帮助,还会影响你的心情。

可能你会疑问,高质量的中文内容也是有的呀?这点我并不反对,但是采集站的 seo,往往做的比独立博客好,这点我相信你也明白。甚至有的搜索引擎,一直在做劣化中文搜索的事情,我不说大家也知道吧?

当然,不谈场景,就是在耍流氓!

但是我相信,每位读者应该都知道何时用中文搜索,何时用英文搜索,以及如何选择搜索引擎吧😁

那如果你要问,我用英文搜索,也有很多内容农场呀,有时候也搜不到东西。别急,咱们接着往下看!

基本的英文搜索技巧

最简单的,能立即想到的,绝对是:关键词+引导词

比如我们可以搜索:Java Tutorial

给大伙儿列举一些常用的引导词:

  • 概述(Overview,Introduction,Getting Started)
  • 指南(Tutorial,Guide,Howto)
  • 功能参考(Reference,Index)

可能用到的逻辑关系:

与(一般空白即可)、或(OR)、非(-)

举个例子:Java -csdn 🤣

科学上网

为什么我们需要科学上网?

这里就不讨论敏感的因素了,单纯的从学术以及上网的舒适度来考虑。

上面刚提到了,国内的搜索引擎,体验确实有点差,尤其是竞价排名和采集站,导致第一页基本上都不是你想要找到的内容。而诸如 Google 之类的搜索引擎,是可以一定程度上避免的。然后就是一些学习资料、影视资料之类的,也得借助科学上网才能有良好的搜索和使用体验。

这一点在开发程序时,查找资料是深有体会!没有坚挺的工具,只有愿意学习的人!

获取和筛选信息

了解“媒介形态”

媒介形态即媒介的生存状态(包括媒介的外部形态和作为内部结构的传播符号)、生存依据、媒介的传播方式方法(包括受众接受媒介信息的形式和途径)以及由此展示的媒介功能与特征。

比如说当我们要学习 Java 时,可以去B站看视频、也可以看书、看官方文档,还可以去看各种博客,这些都属于“媒介”。

当然,媒介不同的形式,给你带来的“信息”,也是不同的。举个例子,学习 Java 而看视频,确实能让你迅速上手,但是这种被动式的媒介形式,也会不可避免的“抑制”你的思考。视频容易让你的大脑停留在“心里舒适区”,你对事物的思考程度,是远比不上读书的。

书籍这种媒介形式,具有主动性、系统性以及深刻性的优势。像编程这种复制的领域,书籍在“系统性”的方面,优势绝对不亚于其它任何一种媒介。

信息获取渠道

个人信息源在社会信息交流系统中具有重要的作用。人是社会性的物种,一个社会人对信息是有强烈需求的,而信息源会影响你的行动决策。一个低效低质量的信息渠道可能会让你做出很多错误的决策,以至于会让你形成错误的认知。

后面可以聊聊,我是如何构建自己的“信息源”的~

DIKW 模型

DIKW 模型是一个可以很好的帮助我们理解数据(Data)、信息(Information)、知识(Knowledge)和智慧(Wisdom)之间的关系的模型,这个模型还向我们展现了数据是如何一步步转化为信息、知识、乃至智慧的方式。——来源:智库百科

为了更好地理解这个模型,咱们得知道,模型的每一层是干什么的,以及相邻层次之间的差异。下面我就拿大伙儿日常生活中,最不起眼的事来举例子吧,没错,就是——“花钱”。

数据(Data) 与信息(Information) 的区别

你每天都会花钱吧,对不对?你花的每一笔钱,都可以被记录下来,从某种角度来讲,每一条记录,都只是一条数据而已,并没有什么意义。因为这时的数据,还是属于“原始资料”的形态。但是,我们可以把每一天,每个月,每年的数据汇总,然后我们进行相应的处理,是不是就能得到一些信息?

比如说一年的总收入和总支出、哪个月花钱最多,钱都花到哪里去了?这些都是“信息”。我们通过分析原始资料,获得了信息。这些信息分为显性和隐性的,比如:谁?什么(内容)?哪里?什么时间?为什么这样?

单个的数据,其实是没啥意义的,但是当我们将它转化为信息之后,它的意义自然就显现了!

信息(Information) 与知识(Knowledge) 的区别

信息的生命周期太短,以至于你不去主动的消化它,它也会失去意义。比如说,你可以反思下过去一年,有哪些东西,是你买了但没必要买的,而且很贵的东西。对过去一年的消费记录(信息)进行归纳、比较、复盘,使信息中有价值的部分沉淀下来,并和自己本身已经存在的知识体系相结合,那么这部分信息就转变成知识了。

想想读书的时候,老师在讲台上讲解题目时,你突然想到做过的另一个类似的题目,然后进行了比较,这也是信息转化为知识的过程。

知识(Knowledge) 与智慧(Wisdom) 的区别

知识和智慧,我个人的理解就是:知识关注的是过去,智慧关心的是未来。

引用一下专业点的解释:人类基于已有的知识,针对物质世界中运动过程中产生的问题根据获得的信息进行分析、对比、演绎找出解决方案的能力。这种能力运用的结果是将信息的有价值部分挖掘出来并使之成为知识架构的一部分。

结合搜索引擎挖掘网络资源

咱们上面有聊到,为什么要使用英文搜索和科学上网。

但是搜索引擎层面,尽量还是要选择 Google 之类的搜索引擎。并且我建议大家使用 google.com/ncr ,这样就能用国际版(禁用国别重定向),经常逛开源社区的小伙伴应该能见到不少人这么推荐。

网页快照

基本上很多人都碰到过 404重定向,这会给我们造成不好的上网体验。我们可以在地址前面加上 cache: ,比如 baidu.com 变成 cache:baidu.com。或者可以使用互联网档案馆(Internet Archive)进行搜索,查看纯文字版的快照,还可以过滤掉 JavaScript 脚本,也会更安全!

过滤搜索

可以结合搜索引擎语法,进行过滤搜索。比如只查看某网站的相关内容,或者搜索特定的文件。

搜索引擎也搜不到该怎么办?

一般有这么几种情况,可能会搜不到想要的信息:

  • 对应的内容提供商,打算“圈地”,禁止了搜索引擎爬虫爬取。
  • 还没搜录。
  • 你打开方式不对。
  • 内容农场太多,导致你找不到正常的内容,从而丧失耐心!

我这里也给出几种对应的解决方案:

  • 看看别人是怎么搜的(是的没错,你可以搜索相关内容,学习下别人怎么搜索你想搜的这个内容的)。
  • 花时间学一下搜索引擎语法吧。
  • 专业的事,交给专业的工具去做。比如说你想找一本书,有时候搜索引擎并不是最好的解决方案,不妨试试 z-library

数字时代如何管理你的数字隐私

前言

1、盘点近年来重大数据泄露和攻击事件

事件其实是非常多的,这里挑一部分出来。

泄露事件

最近大家最熟悉的,应该是滴滴的事情了吧,大家可以去看一下官方的通报!

攻击事件

2、自查泄露途径

列举一些渠道,你可以自查一下,你的这些信息,都在什么地方使用过/泄露过。

邮箱

手机号

涉及到个人隐私问题,我无法保证读者不滥用,所以手机号自查就跳过了。

常用用户名

对上面查出来的结果不要惊讶,因为数据不一定就是100%正确的。我自己在自查的时候,就发现,自己常用用户名被人抢注的情况。一种可能是别人也想到这个用户名了,另一种可能的情况就是,有人故意而为之。

密码泄露查询

IP/DNS自/反查

  • DNS leak test - 检测 DNS 泄露的网站
  • IPIP.NET 专注 IP 地理位置以及 IP 画像数据的研究、整理与发行

3、隐私信息搜集

涉及到个人隐私问题,我无法保证读者不滥用,所以隐私信息收集就跳过了。我不可能教别人去搜集隐私啊,对吧?

4、保护措施

说了这么多,本期的重点来了。主要是教大家如何最大程度的保护自己的数据隐私,防止自己的隐私泄露。

操作系统

在操作系统层面,凡是可能涉及到隐私的东西,尽量不要用移动操作系统。你可以使用 Windows、macos 和 *unix 操作系统进行这些操作。

  • 不建议使用预装的操作系统。我虽然不反对大家用 Windows 或者 macos 来操作,因为实际上很多读者也就是普通人,所以要考虑到大家的使用习惯和学习成本。因此,这里强调的防范也是相对的。即你可能使用某大公司或银行的服务,但是同时又想规避第三方公司恶意收集你的信息,你就可以从操作系统层面做一些隔离。
  • Linux 操作系统建议选社区主导的发行版。

软件

  • 除非你有充分的理由,否则请去官方下载任何软件,不要在什么XX软件管家、XX下载站下载。这些地方除了会有捆绑软件之外,可能还会携带病毒,甚至提供被篡改的软件安装包给你,毕竟很多读者,并不会去核查安装包的数字签名。
  • 建议使用虚拟机来隔离你的操作系统,保证进一步的安全。我个人比较喜欢用 VMware Workstation 虚拟机,当然 VirtualBox 也挺好用。

更深入的虚拟机颗粒度、虚拟机穿透、虚拟机快照等等知识,这里就不做多讲解了。

  • 使用软件是,如果可以,同类型产品优先考虑开源软件。比如解压缩软件,我就使用的 7zip。
  • 对于一些出了名的垃圾软件,但是你又不得不用的,直接扔虚拟机吧。。。

浏览器扩展

  • HTTPS Everywhere

  • 终结内容农场

  • DuckDuckGo Privacy Essentials

  • Cookie AutoDelete

  • Decentraleyes

  • NoScript

密码学

  • 一定要让你所有的平台的密码都不同!!!
  • 现在很多平台要求大小写+数字+特殊符号+8位以上,所以我建议你可以在脑海中构建一套自己的密码生成系统,将你所有的密码都换成这种形式的。

输入法

  • 尽量不要安装第三方输入法,或者使用开源和来源可信的输入法。

搜索引擎

  • 如果你一定要使用中文搜索引擎,我建议优先使用 Bing ,其次是搜狗。为什么不建议百度呢,主要是体验太差了,广告多,seo恶心,你几乎很难找到有价值的信息。
  • 如果你使用 Google ,那么我建议你把 Google 给 ncr 一下。
  • 你可以使用 DuckDuckGo 搜索引擎来进行隐私搜索。

隐私上网

  • 邮箱

    这里可以使用临时邮箱进行替代,为什么这么说呢。因为你注册某些业务,可能并不需要经常访问,甚至只使用一次,而且这个东西也并不重要,这时你就可以使用临时邮箱了。要说临时邮箱最大的好处,我觉得是有效的避免了垃圾邮件和钓鱼邮件。试想一下这个场景,你的工作邮箱的通知,时刻保持开启,但是无时无刻都在接受着垃圾邮件的轰炸,是不是心情都坏了?

  • IP代理

    很多时候,非必要的上网,其实可以使用代理进行上网,从而隐藏你的真实 IP。而且你可以指定相应的规则,不同的 IP,不同的网站,使用不同的代理。

  • 短信接码

    跟上面的邮箱一样,你注册某项访问,可能只使用一次,并且仅支持手机注册的话,那么你可以使用接码的方式进行,这样也能防止一些垃圾短信。接码,顾名思义是接收短信验证码。而很多接码平台,则提供了接收验证码、收发短消息、短消息通道、接收语言短信等服务。但是,据我了解,这个是有法律风险的,所以还请各位读者不要滥用。

  • 身份生成

    对于某些平台,你可能并不想向他们提供你的真实信息,那么你可能需要生成假的身份信息。虽然国家有相应的法律法规保护着我们,但是总是有些公司会偷偷的贩卖你的信息,甚至有一部分公司,直接是没有保护好你的信息,导致了信息的泄露。

照片信息

首先,我不建议你把自己的照片随便传到网上去,那样肯定是不太好的。如果你非要传,也可以处理下照片,比如照片的 EXIF 信息。而且手指的指纹也要保证能不被检测处理,当然,现在很多人的照片P的她妈都不认识,指纹当然也不会有啦🤣

我觉得,对于普通人来说,想让他们引起对照片的重视的话,可以推荐看看侦探挑战赛

一些建议

身处数字时代,你身边的一切,都可能在贩卖和泄露你的隐私数据。

  • 外卖、快递单号。
  • 发布到网上的照片。
  • 社交媒体关联信息。
  • 你家里的语音助手音箱和其它未经授权的设备。
  • 买卖二手设备时,信息是否删除。
  • 广告商对你进行的追踪。
  • 你电脑/手机上的软件,扫描你的硬盘。
  • 你在社交平台的冲浪信息,在购物平台的消费信息,都有可能被爬取或泄露。
  • 浏览一个陌生网页时,首先想到的应该是查看网页快照。

5、免责声明

1、本文章任何内容仅供学习和研究使用,请勿使用其中所描述的技术手段用于非法用途,任何人造成的任何负面影响,与本人无关。 2、本文章任何内容、新闻皆不代表本人态度、立场,如有侵权,请立即联系我删除! 3、互联网是有记忆的,互联网也并非法外之地!你可以教他人如何防范风险,但是不要教他人干坏事!

最后,本文不是教你干坏事的,而是说希望能引起每个人对自己隐私的重视,防范自己的隐私泄露问题。

❌
❌