跳到主要内容

NodeJS child_process exec原理

· 阅读需 6 分钟

今天工作中,遇到一个难缠的需求:在 nodejs 中用命令行方式运行一段 etl 脚本。数据库是 SAP 公司的 hana,相对冷门,驱动的接口和其他也有区别。需求中用到了await-exec这个库,其背后是 child-process,即用于创建和控制子进程。这个模块允许你在 Node.js 应用中运行外部命令、脚本或其他 Node.js 进程,并与这些子进程进行通信。通过 child_process,你可以执行 shell 命令、启动其他程序,甚至是在独立的 Node.js 环境中运行代码。

child-process 简介(AI)

child_process 是 Node.js 标准库中的一个模块,用于创建和控制子进程。这个模块允许你在 Node.js 应用中运行外部命令、脚本或其他 Node.js 进程,并与这些子进程进行通信。通过 child_process,你可以执行 shell 命令、启动其他程序,甚至是在独立的 Node.js 环境中运行代码。child_process 提供了几种方法来创建和管理子进程:

  1. exec
  • 用于执行一个命令,并且将整个命令的输出缓存在内存中。

  • 适合用于执行简单的命令,例如调用一个 shell 命令并收集结果。

  • 代码示例:

const { exec } = require("child_process");

exec("ls -lh", (error, stdout, stderr) => {
if (error) {
console.error(`执行出错: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
});
  1. spawn
  • 用于启动一个新的进程,并且可以通过流(streams)与该进程进行交互。

  • 适合用于处理大输出或者需要与进程进行持续交互的场景。

  • 代码示例:

const { spawn } = require("child_process");

const ls = spawn("ls", ["-lh", "/usr"]);

ls.stdout.on("data", (data) => {
console.log(`stdout: ${data}`);
});

ls.stderr.on("data", (data) => {
console.error(`stderr: ${data}`);
});

ls.on("close", (code) => {
console.log(`子进程退出码: ${code}`);
});
  1. fork
  • 专门用于创建一个新的 Node.js 进程,并且能够很方便地在父子进程之间传递消息。

  • 适合用于多进程处理的场景,例如将任务分配给多个子进程来并行处理。

  • 代码示例:

const { fork } = require("child_process");

const child = fork("child.js");

child.on("message", (message) => {
console.log("来自子进程的消息:", message);
});

child.send("开始处理");
  1. execFile
  • 类似于 exec,但 execFile 直接执行一个可执行文件,而不是通过 shell。

  • exec 更加安全,因为不涉及 shell 命令解析。

  • 代码示例:

const { execFile } = require("child_process");

execFile("node", ["--version"], (error, stdout, stderr) => {
if (error) {
console.error(`执行出错: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
});

主要用途:

  • 自动化任务 :可以用来执行 shell 脚本或其他命令行工具,自动化各种任务。

  • 与系统交互 :能够直接调用系统命令或运行外部程序。

  • 多进程处理 :通过 forkspawn 创建多个进程来并行处理任务,提升性能。child_process 模块在需要与操作系统或其他程序交互时非常有用,同时也可以用来在 Node.js 中实现多进程并发处理。

痛苦的调试过程

在主进程脚本文件中,调用 cp 的代码片段:

const pipelineRet = await exec(`${tapCMD} | \
$TARGET_CLICKHOUSE_CMD -c $TARGET_CLICKHOUSE_HOME/config.json`);

其中 tapCMD 大致内容是node xxx.js。我发现,在 xxx.js 中无论打多少次 log,控制台都不会显示。我大概猜到是因为不在同一个进程的原因。只有在 throw Error 时,控制台才会打印出错误。可是这样一来,throw error 后面的代码就不会运行,给调试带来极大的不方便。后来,我研究了一下 cp 的 api,发现本不必如此麻烦。

const { exec } = require("child_process");

exec("node xxx.js", (error, stdout, stderr) => {
if (error) {
console.error(`执行出错: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
});

在 xxx.js 执行完毕后(或者意外退出),就会执行后面的回调函数,error 是 Error 加 stderr,steerr 对应的是 console.error 级别,stdout 就是 console.log。如果 error 存在,就会进入 if 中,return 返回,自然不会执行后面的打印 stdout。await-exec库的源代码与上面逻辑类似。

其实,正确的调试方式是 console.error 来取代 log,这样调试起来方便多了。

NoSQL老大-MongoDB的使用

· 阅读需 2 分钟

数据层级关系

mongodb 数据分为三层,分别是 db,collection,document。collection 相当于 table,document 就是一条条的 record。

工具

mongodb 安装时会自带一个名为 mongosh 的 shell,而这个 shell 本质上也是一个 JavaScript 解释器。所以在里面可以直接执行 js 命令。

mongosh

实际使用中似乎很少用 mongosh 的,不太直观。市面上有一些 mongo 的可视化管理工具,比如官方的 MongoDB Compass,Navicat for MongoDB 等。推荐用免费的 MongoDB Compass。

CRUD

updateOne/updateOne 主要区别

  • 更新数量 :

    • updateOne 只更新第一个匹配的文档。

    • updateMany 会更新所有匹配的文档。

  • 使用场景 :

    • 使用 updateOne 当你只想更新单个文档,通常是对唯一标识(如 _id)进行操作时。

其他的 one 和 many 都类似。

查询操作的灵活性

mongoDB 的 json 式查询带来了很多灵活性,我们可以像编程一样写查询。比如说操作符:

const cursor = db.collection("inventory").find({
status: { $in: ["A", "D"] },
});

aggregation pipeline

聚合管道,不只有聚合操作。实际上是一个数组,每个元素为 stage,并按排列顺序执行。

db.orders.aggregate([
// Stage 1: Filter pizza order documents by pizza size
{
$match: { size: "medium" },
},

// Stage 2: Group remaining documents by pizza name and calculate total quantity
{
$group: { _id: "$name", totalQuantity: { $sum: "$quantity" } },
},
]);

计算机路径分隔符之辩-斜杆和反斜杠

· 阅读需 6 分钟

You:

请从历史和用法等角度,告诉我斜杆和反斜杠作为路径分隔符的区别

ChatGPT:

斜杠(/)和反斜杠(\)作为路径分隔符的区别主要在于它们的历史背景和在不同操作系统中的使用。

历史背景

斜杠(/)

  • 斜杠最早在 Unix 系统中作为路径分隔符使用。Unix 系统是在 20 世纪 60 年代末和 70 年代初开发的,它采用斜杠来分隔目录和文件。这个传统一直延续到了现代,包括 Linux 和 macOS 等 Unix 系操作系统。

  • 互联网的统一资源定位符(URL)规范也采用斜杠来分隔路径。例如,在网址中使用http://www.example.com/path/to/file

反斜杠(\)

  • 反斜杠作为路径分隔符是由 Microsoft 在其操作系统中引入的,最早在 MS-DOS 系统中使用。MS-DOS 是 20 世纪 80 年代初开发的,而在那个年代,计算机技术尚未完全标准化。微软选择了反斜杠,因为在当时的 CP/M 操作系统中,斜杠被用作命令行开关标志(例如dir /w)。

  • Windows 继承了 MS-DOS 的传统,因此在路径中继续使用反斜杠。例如:C:\Program Files\MyApp\file.txt

用法区别

  1. 操作系统依赖
  • 在 Unix 系系统(如 Linux、macOS)和相关的应用程序中,斜杠(/)是默认的路径分隔符。

  • 在 Windows 系统中,反斜杠(\)是默认的路径分隔符。

  1. 可互换性
  • 在现代的 Windows 系统中,斜杠(/)和反斜杠(\)通常可以互换使用,因为许多 Windows 应用程序和 API 都能够识别和处理斜杠。例如,C:/Users/UserName在 Windows 中也能正常工作。

  • 然而,在 Unix 系系统中,反斜杠(\)不是有效的路径分隔符,它通常被用作转义字符。

  1. 编程语言和工具的处理
  • 在许多编程语言中,反斜杠(\)作为转义字符使用。例如,在字符串中使用\n表示换行。因此,在表示路径时需要双反斜杠(\)或使用原始字符串字面量(如 Python 中的 r""),以避免转义问题。

  • 使用斜杠(/)通常可以避免这些问题,尤其是在跨平台开发时。

总结来说,斜杠和反斜杠的使用主要取决于操作系统和历史习惯。在跨平台开发中,推荐使用斜杠(/)以获得更广泛的兼容性和避免潜在的转义字符问题。

双反斜杆

两条反斜杠(\)在路径中也有特定的用法和意义,主要涉及编程语言中的字符串处理和特定环境中的路径表示。

双反斜杠的用法和联系

  1. 转义字符
  • 在许多编程语言中,反斜杠(\)是一个转义字符,用于表示特殊字符或控制字符。例如,\n表示换行,\t表示制表符。在这种情况下,如果需要表示实际的反斜杠字符,就需要使用双反斜杠(\)。

  • 举例来说,如果你在 Python 中想要表示 Windows 路径C:\Program Files\MyApp,可以写成"C:\\Program Files\\MyApp"。第一个反斜杠表示转义,第二个反斜杠才是实际的字符。

  1. 字符串字面量中的路径表示
  • 在一些语言(例如 C#和 Python)中,有一种特殊的字符串字面量形式,允许不对反斜杠进行转义处理。在 Python 中,这称为原始字符串字面量(raw string literal),以r开头,例如:r"C:\Program Files\MyApp". 在这种情况下,双反斜杠并不需要,因为反斜杠不会被视为转义字符。
  1. 网络路径
  • 在 Windows 中,两条反斜杠有时用来表示网络路径或共享资源。例如,\\ServerName\ShareName表示网络服务器ServerName上的共享文件夹ShareName。这种用法在 Unix 系系统中没有对应物。

区别和联系

  • 区别 : 单个反斜杠通常作为路径分隔符使用,但在字符串中它也用作转义字符。为了表示实际的反斜杠字符或避免转义,常常需要用到双反斜杠。

  • 联系 : 双反斜杠的使用通常是为了避免或正确处理转义字符的问题,无论是表示实际的反斜杠字符还是在路径字符串中避免转义。它们在 Windows 路径和网络路径表示中也扮演着重要角色。

总之,两条反斜杠主要用于编程语言中的字符串处理以及在特定环境下表示网络路径或共享资源。

React 框架的使用体验

· 阅读需 6 分钟

为什么学

每次学习新技术,我都习惯问自己这个问题。那么,换一句话说,我们为什么需要借助框架来完成前端开发(而不是 vanilla JS)?以下是 chatgpt-4o 对这个问题的回答:

从更底层的角度来看,前端框架解决了开发过程中一些核心的复杂性和挑战。以下是几个关键方面:

  1. DOM 操作的抽象
  • 原生 JavaScript :直接操作 DOM 通常需要编写大量代码,且容易出错。例如,创建、插入、删除和修改 DOM 元素需要详细处理每个步骤,这在复杂应用中会变得难以维护。

  • 框架 :通过抽象层(如虚拟 DOM),框架简化了 DOM 操作。例如,React 的虚拟 DOM 机制会在状态变化时高效地更新实际 DOM,而开发者只需关注状态的变化和 UI 的呈现。

  1. 状态管理
  • 原生 JavaScript :管理复杂应用的状态可能需要大量的事件处理和全局变量,容易导致代码混乱和难以调试。

  • 框架 :提供了系统化的状态管理方案。例如,Redux 和 Vuex 等状态管理库帮助开发者集中管理应用状态,使得状态变化更可预测、更易调试。

  1. 组件化开发
  • 原生 JavaScript :复用 UI 组件通常需要手动编写和管理,难以保证组件的独立性和复用性。

  • 框架 :提供了组件化开发的基础设施。开发者可以创建独立的、可复用的组件,这些组件可以在不同的地方使用,并且易于维护和扩展。

  1. 数据绑定
  • 原生 JavaScript :实现数据绑定(即数据变化自动反映在 UI 上)需要手动编写代码,通常通过事件监听和 DOM 操作来实现。

  • 框架 :例如,Vue 的双向数据绑定机制和 React 的单向数据流,使得数据和 UI 同步更加直观和简单,减少了手动更新 DOM 的繁琐工作。

  1. 模块化与依赖管理
  • 原生 JavaScript :管理代码模块和依赖关系通常需要借助外部工具(如 Webpack),且需要手动配置和管理。

  • 框架 :许多框架自带模块化和依赖管理系统。例如,Angular 内置的模块化系统和依赖注入机制,使得代码组织和依赖管理更加简便。

  1. 性能优化
  • 原生 JavaScript :手动优化性能通常需要深入了解浏览器的工作原理和具体的性能瓶颈。

  • 框架 :例如,React 的虚拟 DOM 和 Vue 的渐进式增强机制,帮助开发者自动处理许多性能优化问题,使得应用更加高效。

  1. 开发工具和生态系统
  • 原生 JavaScript :虽然有许多开发工具可以使用,但需要单独配置和集成。

  • 框架 :通常提供了一整套开发工具和生态系统。例如,React 开发者工具、Vue CLI 等,帮助开发者快速搭建、调试和部署应用。

通过这些底层机制,前端框架解决了许多原生 JavaScript 开发中的复杂问题,使得开发流程更加高效、代码更加健壮和可维护。

我的感受:前端 JS 最烦人的就是 dom 操作,非常频繁且 api 写法冗长。前端框架把人从 dom 操作中解脱出来,只要关注数据的变化即可。还有一个就是组件化开发,html 的树状结构意味着一定有很多功能相同的节点,这意味着需要抽取出来作为组件,这一点在 JS 中同样不好实现。

在前端领域,各个方向的工具的选择项都非常丰富。然而,在框架层面,就是 React 和 Vue(暂不讨论 Angular 和 Svelte)。客观上讲,这两个无优劣之分,只有适合与否。React 在海外的流行程度应该远超过 Vue。另外,我的感受是,渲染数据时,在 JavaScript 中写 HTML(React)比在一个框架定义的类 html 模板里更顺手,后者需要记住自定义的标签。

React 是框架吗

严格上来说,React 不是框架。因为 react 并没有针对前端项目的一个完整解决方案(包括路由、状态管理等)。Vuejs 作为 React 的竞争对手,就是一个开箱即用的框架。

react 的核心就是 react 和 react-dom 两个库。

expressjs 框架的使用体验

· 阅读需 3 分钟

expressjs 的官网介绍:一款快速的、无预设的、极简的 web 框架。Nodejs 的很多框架正是基于 expressjs。作为一个极简框架,我打算在一篇文章中介绍它和我的使用感受。

hello-world

项目创建这一步就体现了 expressjs slogan 中的极简。其他框架都要借助npx或者npm install xxx -g等脚手架工具,生成一个完整(庞大)的项目。然而,express 只需要简单的npm init -y来初始化项目,然后npm install express,执行完后目录中只有 index.js, node_modules, package.json,非常的清爽。同时这也体现了无预设的特性,既不规定开发者要按什么规范来组织代码文件架构,不干涉技术选型。可以说,掌握了 expressjs,就掌握了 web 后端开发的众多核心概念。

.env 文件的创建

一直以来关于 nodejs 项目的环境变量,我都有一个误解,就是要真的跑到 os 的环境变量管理窗口去添加。实际上,只需要遵守下列步骤就可以了:

  • 项目根目录创建.env文件,填上变量,并在代码中使用process.env.xxx来引用
  • package.json的启动命令中,加上参数--env-file=.env
  • 重启项目

指定模块定义方式

两种模块定义方式,commonjs 和 ES6,可以在package.json加一个 type 字段,如:`type: module"。默认的是 commonjs。

最佳实践

因为 express 本身不规定任何开发模式,所以遵守最佳实践是很重要的。有几个必要的文件夹:

  • routes: 存放路由
  • middleware: 中间件
  • controller:handle function

上面这些组件可以在主文件中被导入并使用。

TypeScript 类型推断

· 阅读需 4 分钟

类型声明是 TypeScript 的灵魂所在。但是,对于之前只接触过 JavaScript 的人,不是逢类型必加。类型推断可以让 TS 写的更像 JS,哈哈。

TypeScript 的类型推论机制可以自动推断变量、函数返回值、参数等的类型,使代码更加简洁,同时保持类型安全。这有助于减少显式类型注解的需求,提高开发效率。以下是 TypeScript 类型推论的一些关键用法和示例:

1. 变量类型推论

当你声明一个变量并对其进行初始化时,TypeScript 会根据赋值的值来推断变量的类型。

let number = 42; // TypeScript 推断 number 的类型为 number
let text = "Hello, world!"; // TypeScript 推断 text 的类型为 string
let isDone = true; // TypeScript 推断 isDone 的类型为 boolean

2. 函数返回值类型推论

TypeScript 可以根据函数的返回值来推断其返回类型。

function add(a: number, b: number) {
return a + b; // TypeScript 推断返回类型为 number
}

3. 函数参数类型推论

在函数调用时,TypeScript 可以根据传递的实参来推断函数参数的类型,尤其是在使用回调函数时。

let numbers = [1, 2, 3];
numbers.forEach((number) => {
console.log(number); // TypeScript 推断 number 的类型为 number
});

4. 上下文类型推论

TypeScript 可以根据上下文来推断某些表达式的类型,例如事件处理器、回调函数等。

document.addEventListener("click", (event) => {
console.log(event.target); // TypeScript 推断 event 的类型为 MouseEvent
});

5. 泛型类型推论

在使用泛型函数或类时,TypeScript 可以根据传递的类型参数来推断具体的类型。

function identity<T>(arg: T): T {
return arg;
}

let output = identity("myString"); // TypeScript 推断 output 的类型为 string

6. 解构赋值类型推论

在解构赋值时,TypeScript 可以根据解构的对象或数组推断其类型。

let point = { x: 10, y: 20 };
let { x, y } = point; // TypeScript 推断 x 和 y 的类型为 number

示例:综合使用类型推论

以下是一个综合示例,展示了类型推论在变量、函数返回值、参数以及上下文中的应用:

// 变量类型推论
let age = 30; // TypeScript 推断 age 的类型为 number

// 函数返回值类型推论
function greet(name: string) {
return `Hello, ${name}`; // TypeScript 推断返回类型为 string
}

// 函数参数类型推论
let names = ["Alice", "Bob", "Charlie"];
names.forEach((name) => {
console.log(greet(name)); // TypeScript 推断 name 的类型为 string
});

// 上下文类型推论
window.addEventListener("resize", (event) => {
console.log(event.target); // TypeScript 推断 event 的类型为 UIEvent
});

总结

类型推论是 TypeScript 的强大特性之一,能够在不显式声明类型的情况下提供类型安全和自动补全。这使得代码更简洁,同时减少了类型注解的冗余,提高了开发效率。尽管类型推论可以处理大多数情况,但在某些复杂场景下,显式声明类型仍然是必要的,以确保代码的可读性和维护性。

JavaScript 事件循环机制

· 阅读需 4 分钟

在 JavaScript 中,任务的执行顺序是基于事件循环(Event Loop)机制。理解这一机制对于编写高效、非阻塞的 JavaScript 代码至关重要。JavaScript 的执行模型可以分为以下几部分:

  1. 调用栈(Call Stack)

  2. 任务队列(Task Queue)

  3. 微任务队列(Microtask Queue)

  4. 事件循环(Event Loop)

调用栈(Call Stack)

调用栈是 JavaScript 引擎的一部分,用于跟踪函数调用的执行顺序。每当一个函数被调用时,它会被推入调用栈;函数执行完毕后,会从调用栈中弹出。调用栈是同步执行的,即一个函数必须执行完毕后,下一个函数才能执行。

任务队列(Task Queue)

任务队列用于存放异步任务的回调,如 setTimeoutsetInterval。当异步任务完成后,对应的回调函数会被放入任务队列中等待执行。

微任务队列(Microtask Queue)

微任务队列用于存放微任务,如 Promise 的回调函数和 MutationObserver 的回调。微任务具有更高的优先级,会在当前调用栈执行完毕后立即执行,优先于任务队列中的任务。

事件循环(Event Loop)

事件循环不断地检查调用栈是否为空,如果为空,它会检查微任务队列是否有任务需要执行。如果微任务队列为空,事件循环会从任务队列中取出第一个任务并执行。

执行顺序示例

通过以下代码示例,可以更清楚地了解 JavaScript 的任务执行顺序:

console.log("Start");

setTimeout(() => {
console.log("Timeout callback");
}, 0);

Promise.resolve().then(() => {
console.log("Promise callback");
});

console.log("End");

执行顺序如下:

  1. console.log("Start") 被推入调用栈并执行,输出 Start

  2. setTimeout 被推入调用栈并执行,注册一个回调函数,并设置 0 毫秒延迟。回调函数被放入任务队列。

  3. Promise.resolve().then 被推入调用栈并执行,注册一个微任务回调函数,并放入微任务队列。

  4. console.log("End") 被推入调用栈并执行,输出 End

  5. 调用栈清空后,事件循环检查微任务队列,发现有一个微任务。将微任务回调 console.log("Promise callback") 推入调用栈并执行,输出 Promise callback

  6. 微任务队列清空后,事件循环检查任务队列,发现有一个任务。将 setTimeout 的回调函数 console.log("Timeout callback") 推入调用栈并执行,输出 Timeout callback

最终输出顺序为:

Start
End
Promise callback
Timeout callback

总结

JavaScript 的任务执行顺序是基于事件循环、调用栈、微任务队列和任务队列的协同工作。理解这些机制有助于编写高效、非阻塞的代码,并避免常见的异步编程问题。

2023年 StackOverflow 开发者调查报告说了啥

· 阅读需 5 分钟

偶然想起,大名鼎鼎的 stackoverflow 报告一直没见过原版,关于它的信息都是二手的。虽然在这个时间点(2024 年过半)有些尴尬,但是还是有读一读的热情。报告很长,我就节选一些我感兴趣的部分做个摘录,并结合自己的印象来谈谈感受。

Developer profile

这部分是开发者画像。绝大部分调查者来自欧美+印度。其中 5 年以上经验的人占了绝大多数。角色这块,依旧是全栈、后端、前端占了前三甲。老外的全栈真的普遍,这点和国内真是显著不同。

Technology

编程语言

职业程序员,HTML/CSS、Javascript、SQL 是绝对统治级别的前三,因为有了这三样,绝大部分的网站都能开发。全栈几乎可以和这三个划等号。4-6 位也比较有意思,分别是 python、typescript、shell。Python 能处在这个位置,除了它自身的简单易用,也是它作为很多人的入门语言形成的惯性,以及它在数据分析、机器学习领域的不可替代性造成的。Typescript 的使用率能和 Python 并驾齐驱,倒是我没有想到的。我想这是因为 JavaScript 语言的流行性极大带动了 ts 的发展。最后的 shell,毫无疑问它的场景在于服务器运维,也是全栈开发者工作中必须面对的场景。未来我打算学的 Golang, 2023 年的排名在 12,落后于 C。

数据库

数据库和我现在做的工作高度相关,感受多一些。网上对数据库的分类有很多,比如 oltp 和 olap。但是我这边的分类的话,更倾向于行数据库、列数据库和混合型。当然,redis 这种内存数据库不在这个范围内。正是因为存储方式的不同,导致不同数据库产品有不同的应用场景。比如最流行的 pgsql 和 mysql,都是行存储,因此属于 oltp。clickhouse 为列存储,故为 olap。近些年更有 tidb 这种高性能数据库,可以在两种存储方式之间切换,所以既可以当作业务数据库,也可以当作分析数据库。

其实如果紧紧围绕业务和分析两大需求,还有别的解决方案。比如前司,pgsql 读写分离,一个 master 配上三个 slave,大屏这种纯分析展示的场景,就从只读数据库去读取。不过仅仅能达到不影响主业务的程度,复杂查询对数据库的压力大,速度也慢。

上面说的其实都属于基于 SQL 查询语言的表格类型数据库。分类上与之平级的还有 nosql 数据库。其下的数据库一般和情景紧密绑定,比如 redis 在高并发、elasticsearch 在日志检索、mongodb 在松散数据存储。属于是有了这些需求,才会去用。

云平台

AWS 是遥遥领先第二的存在。AWS 几乎也成了云平台标准的制定者。比如 S3 存储,都是 AWS 最开始制定的。看来,以后想要出海工作,AWS 也是必须要接触熟悉的。

编程框架

前五名(React/Nodejs/Jquery/Angular/Express)全是基于 JavaScript,这也应证了 javascript 最流行的结果。这里面,前端框架独占其三,这说明了,前端生态圈的旺盛。

后记

这份报告比较全面,远不止上述内容。如果要阅读全文,这里给出全文链接:


2023 Devoloper Survey https://survey.stackoverflow.co/2023/#methodology


K8S Services解释

· 阅读需 3 分钟

从我个人角度来看,service 是 k8s 最容易出问题的地方。网络不通是最常见的问题,想要排查网络,就要对 service 有一个深入的理解。