数据库系统的核心作用在于高效地存储和管理数据，并支持从海量信息中快速检索所需记录。传统的 SQL 关系型数据库在数据规模持续增长的情况下，容易遭遇单机性能瓶颈。目前提升性能的主流做法多依赖垂直扩展增强单机硬件能力，而非水平扩展增加节点以线性扩容。由于其架构限制，关系型数据库在弹性扩展性方面存在障碍，难以充分适应多节点分布式环境。同时为满足 ACID 事务特性，这类系统在支持分布式事务处理时也面临一定的技术挑战。相比之下，NoSQL 数据库更适用于大规模数据存储和高吞吐、低延迟的应用场景。由于 NoSQL 不受固定数据结构和关系的限制，具有更灵活的扩展能力，并且支持节点的动态扩展，从而更好地适应分布式环境。无论是关系型 SQL 数据库还是 NoSQL 数据库，它们都有一个共同的核心组件存储引擎，这是数据库设计与实现的关键核心部分。目前数据库存储引擎可以分为两大类，一类是基于内存的 （In-Memory）实现，例如 Redis 和 Memcached 这类的将数据全部存储在内存中；另外一种则是类似于 MySQL 的 InnoDB 和 LevelDB 这种将数据存储在磁盘中。两者都有各自的优势和劣势，基于内存的存储引擎通常存储的数据量较少，但其访问速度往往比基于磁盘的存储引擎快好几个数量级，而基于磁盘的存储引擎访问速度较慢，但存储数据量要比内存引擎多几个数量级。通常内存的价格要比磁盘的贵很多，这也是在选择存储硬件时要考虑的成本问题。存储引擎的开发者需要权衡读写性能和硬件成本的同时，重点在于如何高效存储数据和检索数据。数据库的存储引擎相当于在操作系统文件系统之上，建立的一套用于数据组织、存储与查询的逻辑管理层，最终实现是依赖于操作系统的文件系统 API 接口的，最终数据是被持久化到磁盘文件中保存，这个就对应着 ACID 中的 Durability 数据持久性。持久性（Durability）是存储引擎最为至关重要的功能实现，目前很多基于内存版本的 NoSQL 数据库例如 Memcached 和 Redis 在这方面做的就比较差，在 NoSQL 服务器运行过程中突然崩溃断电就会导致数据没有被持久化存储的情况，从而导致数据丢失影响到上层的业务程序。[scode type="yellow"]所谓的持久性就是如何将计算机运算的结果持久化存储在一些物理介质中。[/scode]存储是现代计算机最核心的功能之一，计算机中的程序和文件以数据块的形式保存在各种存储介质中，主要可分为持久化存储和非持久化存储两类。非持久化存储通常指内存（如 RAM），其特点是在断电或关机后，数据会被清除。尽管容量通常小于持久化存储，但其高速读写性能使其广泛应用于程序运行过程中的数据处理和临时缓存。相比之下，持久化存储如硬盘、固态硬盘、DVD、磁带等，能够在断电后保留数据，适用于长期保存程序和文件信息。[scode type="yellow"]鉴于数据可能存储在多种物理介质（如磁盘、SSD、光盘等）上，主流的操作系统需要提供一致的抽象接口，以屏蔽底层硬件差异，使用户能够高效、统一地进行文件的读写、创建与删除等操作。当前主流操作系统均实现了完整的文件系统，开发者可以通过如 POSIX 这样的标准 API 接口来访问和管理存储设备，而无需关心具体设备的物理实现方式。尽管操作系统的文件子系统通过标准如 POSIX 提供了对存储设备的访问能力但仍存在一些限制；例如存储性能受限于设备的物理读写速度，同时文件系统通常采用页缓存 Page Cache 或写缓冲机制，这意味着数据写入操作并不会立即持久化到磁盘，一旦系统发生故障或意外断电，可能会导致数据丢失，影响系统的可靠性。[/scode]基础架构综上所述，针对这些场景的问题 UrnaDB 采用了基于 Log-Structured Megre Tree 日志结构化文件系统的存储引擎实现，存储引擎会以 Append-Only Log 的方式将所有的数据操作写入到数据文件中。同时 UrnaDB 为了高速查询检索数据记录，存储引擎会将数据记录索引信息全部保存在内存中，从而实现高效快速的查询目标数据记录。这样的设计的好处是能以磁盘最大写入性能进行写入数据，并且还能减少读取磁盘索引所需要的时间，通过一次索引定位来读取数据记录，写入和查询流程图：其核心持久化机制基于预写日志 Write-Ahead Logging 简称 WAL ，在对数据执行任何操作前，都会先将操作记录写入 WAL 日志文件。WAL 文件不仅承担持久化的角色，也作为主要的数据存储载体。在数据库进程崩溃后，只需从 WAL 日志中顺序读取各条 Segment 记录，即可高效恢复内存中的索引结构。在 UrnaDB 中对这些 WAL 数据文件有一个统称为叫 Region 文件，这些文件有单个固定大小限制，当一个数据文件写满之后就会被关闭，会重新创建一个新的 Active 活跃的 Region 文件进行数据记录的进行写入。被关闭的 Region 文件会被视为冷数据文件，随着数据库不间断长时间运行 Region 文件会不断递增逐渐占用磁盘空间。此时数据库进程就需要对旧的 Region 文件执行压缩 Compaction 和定期清理，以降低存储压力并提高查崩溃数据恢复时的启动效率，压缩流程原理图：目前工业级的存储系统中，RaikDB 是一个采用类似模型实现的数据库产品，RaikDB 是基于 Amazon Dynamo 论文的设计理念构建而成。其 RaikDB 底层也是采用的顺序写入的日志式存储引擎，并将其命名为 Bitcask 存储模型，以提升写入性能和读写效率。该模型与 UrnaDB 所采用的存储引擎在设计理念上颇为相似。Bitcask 存储引擎在执行数据压缩 Compaction 时会生成 Hint 文件，其作用是在存储引擎重启时，辅助快速重建内存索引，从而避免每次启动都需全量扫描数据文件，显著提升系统的启动效率。然而 Hint 文件仅在 Compaction 过程中生成，所记录的只是当时内存索引的快照，无法反映实时状态。[scode type="yellow"]针对这一问题 UrnaDB 在存储引擎中进行了改进，将 Hint 文件机制替换为 Checkpoint 功能，系统会在固定时间间隔内，定期将当前内存索引持久化为 checkpoint 文件写入磁盘。当服务器进程发生异常崩溃，也可通过读取 checkpoint 快照快速恢复内存索引状态。在实际恢复流程中，UrnaDB 会优先加载最近一次的 checkpoint 快照，并从该时间点之后的 Region 文件中继续回放增量数据。通过这种方式，有效提升了系统启动恢复的速度，减少了启动耗时，同时兼顾了数据一致性与运行效率。[/scode]在该存储引擎设计的基础上，UrnaDB 构建了内置的 HTTP 服务器，并引入了多种抽象化的数据结构层。整体架构如下图所示：这些模块相互协作构成了一个完整的单机版 NoSQL 数据库，实现了从底层存储到服务接口的一体化设计。检查点技术[scode type="yellow"]由于 UrnaDB 数据库存储引擎采用将所有索引数据存储在内存中的设计，一旦数据库进程意外崩溃，内存中的索引信息将会丢失，未能持久化到磁盘。这样在下一次启动时，系统需要重新扫描所有数据文件，以重建内存中的索引结构，如果数据文件体量较大，将导致启动过程变得缓慢，启动耗时较长。[/scode]Checkpoint 功能机制会通过定期将内存中的索引状态保存到磁盘，在发生异常重启时即可快速加载 checkpoint 文件，从而大幅缩短数据库的启动时间。开启 checkpoint 功能可以在数据库崩溃后显著加快启动恢复的速度。然而 checkpoint 文件的生成时间间隔设置同样需要谨慎权衡。如果生成间隔过短，将导致 checkpoint 文件过于频繁地写入磁盘，占用大量 I/O 资源，可能影响数据库的整体性能；而如果间隔设置过长，崩溃恢复时仍需扫描大量 Region 数据记录，反而无法发挥 checkpoint 的优势，导致启动时间较长。因此合理配置 checkpoint 的生成频率，需在性能开销与数据恢复效率之间做出平衡，建议根据实际业务的数据写入频率和系统负载，动态调整该参数以获得最佳效果。磁盘垃圾回收器[scode type="yellow"]由于 UrnaDB 存储引擎采用顺序日志写入结构，随着系统运行时间的增长，每个 Region 区域对应的磁盘数据文件会不断增大。默认情况下 Region 文件中的每条数据记录未经过压缩，导致磁盘空间占用随数据量增加而持续上升。为了解决这一问题 UrnaDB 存储引擎内置了数据压缩算法，启用该压缩器功能后，可以显著减少磁盘文件的存储体积，从而有效降低存储成本。[/scode]在配置 Region 区域的垃圾回收功能时，需要综合考虑多种因素。若不启用该功能，随着系统长时间运行，磁盘空间占用将持续增长，甚至可能过高，影响系统稳定性。若启用垃圾回收，则必须合理设定其执行周期：周期过长：会导致空间回收不及时，磁盘占用上升，同时在压缩 Region 时耗时较长。周期过短：会导致垃圾回收过于频繁，增加系统资源开销，进而影响存储引擎的整体性能。在 UrnaDB 中垃圾回收器的执行周期同样依赖于 Time 触发，并且其执行过程会占用一定的系统资源。如果采用并行执行机制，这段时间对外部应用基本无感，系统可持续对外提供服务。这种设计理念与许多现代编程语言的垃圾回收器类似，UrnaDB 的 Region 区域划分为 Active Region 和 Read Region，其架构与 Java 虚拟机（JVM）中的垃圾回收机制具有相似性。当垃圾回收开始运行时，它不会对 Active Region 中的活跃数据进行操作，而是仅针对旧的 Read Region 区域执行垃圾分析与清理，从而避免对当前读写路径造成干扰，不会出现类似于编程语言虚拟机中的 Stop-The-World 世界时钟停止问题，减少压缩磁盘空间的抖动，保障数据库稳定性。时间就是一个问题[scode type="yellow"]在整个 NoSQL 数据库的设计过程中，许多复杂问题的根源其实都与物理世界中的 Time 有关。当前的 UrnaDB 实现为单机版本，因此主要面临的是本地磁盘 I/O 和线程调度带来的延迟问题。然而一旦在未来引入多个存储节点，发展为分布式架构，就必须应对网络延迟、时钟同步、事务一致性等与时间密切相关的挑战。因此无论是当前的 NoSQL 单机实现，还是面向未来的分布式扩展，如何合理建模和管理时间 Time 维度，始终是数据库架构设计中的核心研究问题之一。[/scode]在单机版本中还面临另一个挑战如何为数据记录提供过期时间（Time-To-Live）支持，尽管 TTL 是常见的需求，但其实现往往较为复杂，目前主流 NoSQL 数据库普遍采用惰性删除机制，即仅在数据被访问时才判断其是否过期并删除，这种方式依赖于外部读取请求的触发，可能导致过期数据长期滞留在存储中。因此如何合理权衡 Checkpoint 的执行频率与垃圾回收的触发时机？是目前 UrnaDB 数据库系统优化的重要考量。如果在数据高频读写期间执行垃圾回收操作如压缩磁盘空间，可能会带来短暂的读写延迟。同时存储引擎在数据迁移和压缩过程中，也可能导致磁盘空间临时占用上升，进而影响整体性能和资源利用。对此我更推荐采用主动回收机制，通过周期性运行的后台垃圾回收线程，专门处理过期数据的清理任务，该方式可以更好地控制存储空间的占用，提升系统的可预测性与稳定性。
在现代社会网络通信的隐私和安全性已成为人们日益关注的重要话题，无论是浏览购物网站、在线银行交易，还是日常的社交媒体互动，我们的个人信息都面临着潜在的风险和威胁。在这篇文章中，将探讨在浏览购物网站时，如何保护我们的隐私和确保信息安全，是谁在保护我们的数据隐私和通信的安全性。在谈论网络通信的安全性之前，让我们回顾一下人类在通信保密性的发展史。通信的保密性自古以来就是一个重要的问题，人类不断发展新的方法来保护信息的隐私和安全，最早的加密技术可以追溯到古代文明，古希腊和罗马使用了各种简单的加密方法，如凯撒密码（Caesar Cipher），其中每个字母被替换为其后固定位置的字母。凯撒密码也称为移位密码（Shift Cipher），是以古罗马将军 Julius Caesar 的名字命名的，据说凯撒在军事通信中使用这种密码来加密敏感信息。在战争期间使用这种加密方法来保护军事命令和机密信息，确保即使信息被截获，敌人也无法轻易破译，凯撒通常使用 N <= 25 位移位（即每个字母向后移 N <= 25 个位置）来加密消息。当消息被敌军获取了，敌军在不知道消息加密的方式情况下，只能得到一堆胡乱排列的字母。凯撒密码的数学公式为如下：凯撒密码的核心原理是通信的双方有一个固定字母顺序表，以这个固定顺序的字母表作为消息加密的基础，通过将原本的消息进行移动位置的方式进行加密。凯撒密码易于破译，尤其是使用频率分析法，通过统计文本中各个字母出现的频率，可以很容易地推测出移位量并解密消息。比如，在加密文本中可以通过寻找加密文本中最常出现的字母来推断移位量，当找到这个字母偏移量就很容易破解出来密文信息了。[scode type="blue"]下面是通过 Java 自定义码表实现凯撒密码加密和解密逻辑：[/scode]public class CaesarCipher { private static final char[] ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); public static void main(String[] args) { String text = "HELLO"; int shift = 3; String encrypted = encrypt(text, shift); String decrypted = decrypt(encrypted, shift); System.out.println("Original: " + text); System.out.println("Encrypted: " + encrypted); System.out.println("Decrypted: " + decrypted); } // 加密函数 public static String encrypt(String text, int shift) { StringBuilder result = new StringBuilder(); for (int i = 0; i < text.length(); i++) { char ch = text.charAt(i); if (Character.isUpperCase(ch)) { // 拿到对应字母的下标 int originalPosition = findPosition(ch); // 找到偏移量之后字母下标，A 至 Z 之间循环 int newPosition = (originalPosition + shift) % ALPHABET.length; result.append(ALPHABET[newPosition]); } else { result.append(ch); // 非字母字符不变 } } return result.toString(); } // 解密函数 public static String decrypt(String text, int shift) { StringBuilder result = new StringBuilder(); for (int i = 0; i < text.length(); i++) { char ch = text.charAt(i); if (Character.isUpperCase(ch)) { int originalPosition = findPosition(ch); int newPosition = (originalPosition - shift + ALPHABET.length) % ALPHABET.length; result.append(ALPHABET[newPosition]); } else { result.append(ch); // 非字母字符不变 } } return result.toString(); } // 查找字符在自定义码表中的位置 private static int findPosition(char ch) { for (int i = 0; i < ALPHABET.length; i++) { if (ALPHABET[i] == ch) { return i; } } return -1; // 应该永远不会发生 } } 这种古代的加密方法，在现代计算机面前已经变得非常脆弱，凯撒密码只是在字母表上进行简单的移位操作，加密之后的密文与明文没有本质上的区别，只是信息的错位混淆处理。本质上密文依然是明文，现代计算机可以轻易地编写程序，快速破解这样的简单加密方式。现代的程序员在存储用户密码时，通常使用 MD5 或者 SHA-1 等不可逆的哈希函数进行存储。这些哈希函数将密码转换成固定长度的哈希值，使得即使密码被窃取，也很难通过哈希值逆推出原始密码，这种方法存储的才是真正的密文。通过将要传递的信息和语言文本信息通过特殊手段进行加密存储，消息在传递过程中能够达到通信完整性和机密性。实际上，这就是一种消息编码和解码的规则。类似于人类语言中使用中文交流的人，听不懂或看不懂用法语表示的语言信息，反之亦然。出现这种情况的原因是缺乏对方语言的编解码器。在计算机通信中，加密和解密的过程也是类似的问题。[scode type="yellow"]如果两台计算机要通过网络进行安全通信，首先需要设计出一套双方能够理解并使用的编解码程序，任何第三者计算机都无法破解或知道编解码的规则。这是保证通信机密性和安全性的前提条件。只有在这个前提条件成立的情况下，通信双方的安全性才能得到保障。总结一下两台计算机在不可靠网络链通信要面临的 3 个问题：如何确认对方真实身份？如何保证数据不能被截获篡改？如何保证数据机密性？这是整个网络通信要面临和解决的问题？从这 3 个问题提取出来的，要保证通信安全的 3 个核心要素：机密性、完整性、可用性 ，换一种说法就是：防窃听 、防篡改 、防伪造 ，满足这些条件才能保证通信隐私安全性。[/scode]对称加密在上面的 Caesar Cipher 故事中使用的是传统对称加密方式，只要密钥不被公开或被黑客窃取就能保证安全性。在现代的计算机中通常都是程序员或者用户设置一段固定长度字符串作为密钥，或者在程序运行期间动态生成一段随机字符串作为加密使用的密钥，这个密钥将被用于针对明文的数据块进行加密。常见的加密算法有 AES 和 DES 算法，可以看作为是一个接收明文数据和密钥返回密文特殊函数，数学函数公式表示为 C=E(P,K) ，对称加解密过程如下图：对称加密一般用于单体的应用或者本机运行的程序中，密钥一般都被存储在运行时的程序内存中，但也需要采取措施保护这些密钥。例如使用硬件安全模块（HSM）或者操作系统提供的安全存储机制（如 Windows 的 DPAPI 或 macOS 的 Keychain）来保护密钥。再者不要长时间使用一个固定的密钥，定期更换密钥，减少单个密钥被破解或泄露后造成的影响，实现自动化的密钥轮换机制，并确保旧密钥的数据能够正确迁移到新密钥上。非对称加密传统对称加密在密钥管理时会遇到如何防止加密密钥泄露的问题，导致对称密钥泄露的原因有很多，例如：密钥过于简单被黑客使用彩虹表攻击；或者将密钥明文硬编码在程序中，当程序被打包为二进制就连密钥一起被打包发布出去了，这样黑客可以通过反汇编技术查看到密钥。[scode type="yellow"]另外一个问题就是非对称加密如果用于在互联网上的两台不同计算机通信，会出现一个问题，两台要进行通信的计算机要协商一个用于数据加密的密钥。如果密钥由互相通信两台计算机中一台生成，那么密钥就要通过网络先发送给另外一台计算机，但在互联网上传输的密钥是明文文本内容，在传输的过程中很有可能被其他的恶意的计算机所截获到密钥，此时就失去了加密通信的意义。[/scode]要建立安通的通信通道就要解决：机密性、完整性、可用性，这 3 个问题。目前计算机网络 OSI 标准中的会话层 TLS（Transport Layer Security）协议，它就是基于非对称加密来保证通信安全性。该协议的前身是网景公司的浏览器为了解决 HTTP 协议在通信过程中明文传输数据时，容易造成数据泄露的问题所开发出的 SSL 协议 （Security Socket Layer）。TLS 协议通过使用对称加密和非对称加密、数字证书和公钥基础设施（PKI），为应用层提供了安全的会话通信机制，开发者在实现安全通信时，通常可以直接利用现有的 TLS 实现来确保通信的安全性。TLS 协议是 TCP/UDP 传输层的安全扩展，旨在确保传输层的数据传输安全。TLS 协议涉及到很多关于密码学和数学相关知识，它基于 RSA（Rivest-Shamir-Adleman）加密算法设计开发的，和传统对称加密解密所使用同一个密钥不同，它需要两个不同的密钥互相配合才能完整加解密的工作，分别为公钥和私钥，工作原理如下图：非对称加密的密钥是一对密钥，由公钥和私钥组成，公钥用于加密数据，生成的密文只能通过对应的私钥进行解密，反之亦然。公钥和私钥之间存在数学上的强绑定关系，确保数据的加密和解密过程是安全。具体运作过程，公钥可以公开分发，而私钥则必须严格保密，以保证只有合法的私钥持有者能够解密通过公钥加密的数据，从而确保数据的保密性、完整性和发送者的身份认证。TLS 的工作原理是首先使用非对称加密技术进行密钥交换，协商出一个用于当前会话的对称加密密钥，随后通信的数据报将通过这个对称密钥进行加密和解密。这样 TLS 协议不仅确保了数据的机密性，还有效地防止了中间人攻击在密钥交换过程中的威胁。TLS 依赖于非对称加密体系，在非对称加密体系中有一个严重容易忽略的问题就是，一对密钥中公钥都公开的，任何人都可以获取得到对方的公钥和使用对方公钥来加密数据发送给对方，这就使得任何人都可以冒充另外一个人的身份来使用公钥，对于解密方来说无非确定是谁使用了自己的公钥。数字签名为了确保通信数据的机密性，首先必须建立信任关系。在 TLS 协议中，使用了非对称加密的 RSA 算法，解决了加密与解密过程中的密钥分发问题。然而，尽管非对称加密在密钥传输上具有优势，它并没有解决公钥可能被伪造或冒充使用的问题。为防止这一风险，TLS 协议通常结合证书颁发机构（CA）来验证公钥的合法性，确保通信双方的身份真实可信。信任关系的建立是保障通信安全的前提，要建立通信双方的信任关系，就要解决公钥公共使用的问题，自己公钥被谁使用了？接收到的数据包是谁使用了自己公钥？解决问题这个方法就是使用数字签名，数字签名原理和日常生活中的纸张上盖章一样。下图为数字签名工作原理：[scode type="yellow"]实现使用不可逆向哈希函数对要发送的数据包进行哈希运算，生产这个数据包内容的摘要信息，也就是对应的哈希码。再使用自己的私钥对摘要信息进行加密，生成对应的密文（数字签名），将其和自定的公钥、数据文件内容打包发送给对方。对方收到数据包取出数据包中的公钥，对数据包中密文（数字签名）进行解密，获得到数据包内容的摘要信息，再使用相同哈希函数对数据包的内容进行哈希运算，得到哈希码，通过比对数字签名的摘要哈希码和本地运算计算出的哈希码来确定数据包真实性和完整性。[/scode]CA 签发证书通过非对加密的私钥和哈希函数对数据进行数字签名，可以解决数据文件归属和署名的问题，接受者可以通过数字签名来确定发送者是谁，但随着而来的一个另外问题是如何获取对应的非对称加密公钥，在验证数字签名过程中需要获取公钥。上面的步骤中将公钥和数字签名、数据文件，打包一起发送给接受者，一个明显的问题是公钥匙明文存储的，且也没有被加密，容易导致数据包中间人截获之后调包更换了公钥。[scode type="yellow"]解决公钥获取最简单一个方式是使用数字签名同样对数据包中的公钥进行签名，公钥使用者在使用公钥之前进行签名验证来确保公钥的安全性。通过对公钥也进行数字签名来解决身份问题，但这样做问题又回到信任问题上，因为整个数据包都是在互联网上传输，也有可能遭受到中间人调包到风险。那如何解决这个问题呢？在现在互联网基础设施中引入了一个公共的公钥签发中心，它可以来管理所有人的公钥，以及确认公钥持有者的身份信息，确保持有者是合法合规的。CA 证书验证中心类似于现在社会中的政府设置安全部门，例如大家信任银行的安全性，都把钱存储在银行里，银行保管着用户存储的钱；通过信任 CA 用户可以信任与其相关的数字证书和公钥，从而建立起安全的通信链路。[/scode]上图为通过权威的 CA 认证证书签发中心颁发任何人的公钥的工作流程。用户首先将自己的非对称密钥的公钥和自己身份信息提交给 CA 中心，CA 中心会对公钥所有者身份进行校验，以确保公钥确实是属于所有者。在 CA 确认了公钥所有者的合法身份之后，会使用自己的私钥对提交者的公钥进行数字签名，并且打包为数字证书文件。当有人需要和公钥所有者通信的时候，只需要通过 CA 来获取被通信者的证书，可以通过 CA 颁发的证书来鉴别公钥，再使用 CA 的公钥对数字证书的内容进行数字签名合法校验，通过验证确认了身份，即可使用非对称加密进行网络通信。通过 CA 认证的证书签发中心就可以完全避免通信双方密钥的配送遭到中间人劫持的问题，一个完整的 CA 签发证书上面包含了 CA 信任链以及使用的签名算法，证书所签发的日前和终止日期，证书所在地球的国家和组织名称。如果当发现私钥泄露的问题了，可以考虑通过 CA 认证中心链来吊销相关的证书[scode type="yellow"]CA 签发的证书只能解决通信双方的身份认证问题，不能保证整个通信信道的安全性，解决这个通信信道安全性问题就要回到上面公钥和对称加密的混合密码体系了。[/scode]通过整合多种密码学技术（如混合加密、数字签名、单向散列和 HMAC）来实现 TLS 网络协议，确保了网络通信的机密性、完整性和身份验证能力。TLS 协议保证从通信双方的端系统从连接建立到通信会话结束整个过程中的安全性，即浏览器和服务器通信所有的数据包在不可信通信链路是经过加密处理的。使用了 TLS 协议进行加密通信，并不意味着就能掉以轻心，这个取决于使用者的习惯和服务器端的维护者如何管理这些客户端发送过来的数据；用户可能使用被二次编译修改过源代码的浏览器，这些浏览器可能就被黑客植入了一些木马后门程序；另外就是当这些数据在服务器端如何保存，没有妥善的保管这些数据，黑客也可能从服务器端漏洞进行窃取这些数据，从而导致数据外泄，这样即使用了 TLS 协议也无法保证敏感性数据不外泄的风险。
Java 这门语言与生俱来的显著特性就是“一次编译，到处运行”，这种功能得益于 JVM 平台的支持，Java 程序通常通过将其打包为 JAR 或 WAR 包，并依赖 JVM 和 Servlet 容器来运行。其底层运行时 JVM 采用 JIT（即时编译）模式来执行程序代码，JVM 会在运行时进行编译优化和动态执行代码，这通常会导致较高的内存占用。这样的好处是采用 JIT 可以热更新和热部署程序，并且 JVM 可以在运行期间对程序进行动态分析，来实时优化程序以达到最好的性能状态。Cloud Native 面临的问题随着云计算的发展，服务端程序的运行和部署方式发生了巨大变化，特别是在云原生（Cloud Native）场景下，服务器程序被打包成一个个容器运行。在这种环境下，传统方式容器化的 Java 程序面临着打包后的镜像文件过大的问题。镜像文件中需要包含 JRE 和各种第三方库，导致镜像体积庞大，占用更多的磁盘空间。目前云计算的计费方式也发生了改变，采用按需分配的实时计费方式，根据程序的内存占用和运行时间计费。这与传统 IDC 自托管式机房中长时间运行服务器程序形成鲜明对比，按需分配的计费方式显然更具性价比，因此优化 Java 程序在容器环境中的运行方式变得尤为重要。Serverless 面临的问题传统的 JVM 工作原理要求程序员先将编写的 Java 程序打包为 JAR 或 WAR 包，这是普通 Java 开发者所能接触到的程序编译和构建阶段。然而当程序运行起来之后，底层的 JVM 会进行许多复杂的操作，包括内存管理、垃圾回收、即时编译等。需要先启动 JRE 中的 JVM 来加载 Tomcat 服务器所需的字节码，再加载 WAR 包中的字节码文件到 Servlet 容器中运行服务，这个过程非常耗时。下图为整个传统 Java 程序从源代码到程序运行各个阶段的工作流程：下图为传统的 Java 运行时执行程序的生命周期，首先应用的开发者将 *.java 源代码编译为 bytecode 和 jar 依赖包通过 maven 构建打包为一个单独的 jar 包。当执行 java -jar xxx.jar 命令时，首先要启动 JRE 中的 JVM 程序，因为 JVM 本身也是一款单独程序对应着 $JAVA_HOME\bin 目录中的 java ，当 JVM 启动之后会在系统中创建一个进程，将要运行的 jar 程序包和 JRE 标准库中 class 文件加载入 JVM 内存中，并且找到 main 方法开始解释执行。当程序运行一段时间之后 JVM 充分获取了 Java 程序的一些执行数据信息，开始对程序进行 JIT 及时编译优化程序，当程序被 JIT 优化之后的程序才能达到和 C/C++ 这种直接编译为 Native Code 程序的性能。在云计算推崇的 Serverless 架构场景下，Java 的 JIT 模式并不适合，Serverless 应用程序需要能够快速启动以响应事件驱动的请求，并且在空闲时自动关机销毁，释放在服务请求期间所使用的内存。然而，这种需求与传统的 Java 程序运行模式背道而驰，使得 Java 在 Serverless 场景中显得不适用，这就是被业界称为的 冷启动 问题。为了解决这一痛点，Oracle Labs 推出一款新的 JDK 产品 GraalVM JDK ，相比传统的 JDK 该产品内置 Java 静态编译工具和一款新的虚拟机，和传统的 JVM 不一样 GraalVM 是支持多语言进行混合编程的虚拟机，这也是它一大亮点。GraalVM 中的静态编译工具不仅是支持将 Java 编译为二进制可执行文件，还支持 JavaScript 和 Python 等其他语言的编译优化功能，本文只关注于 GraalVM 对 Java 程序进行 AOT（Ahead-Of-Time）编译的解决方案。使用 GraalVM 对 Java 程序进行 AOT 编译的解决方案，可以显著缩短启动时间并降低内存占用，使 Java 程序更适合在 Serverless 环境中运行，通过 AOT 编译，编译器生成一个 Native Code 二进制文件，即对应平台架构的机器代码程序。这种方式无需像 JIT 那样依赖于 JVM 运行，在内存和磁盘占用上有显著改善。下图为 AOT 编译工作流程，在编译阶段会程序进行静态分析，通过它内置工具分析 Java 源代码中依赖关系，将其所有依赖和代码执行逻辑都进行提前编译为机器代码，缺点也很明显可能会失去在 JIT 模式下的动态反射功能。如上图，在 AOT 模式下将在运行时的过程放到在程序构建阶段，构建阶段会对 Java 程序执行静态代码分析和依赖可达性分析，将其所有的依赖软件包都编译生产所属平台对应的可执行二进制文件。AOT 编译优化对程序进行 AOT 编译特别适合云应用程序，被 AOT 编译优化之后的程序启动速度足够快，从而缩短启动时间并更直接地水平扩展云服务，内存占用相比 JVM 模式下要少很多，这对于在云中运行的容器初始化的微服务尤其有益。AOT 前提是完全封闭的运行空间，因为它消除了各种代码注入的可能性，例如 2021 年震惊网络的 Log4j 漏洞就是由于利用了 Java 中的动态类加载机制而发生的，在 AOT 编译之后程序就不会发生类似于漏洞。下面这张蜘蛛图详细展示了不同程序运行方式的区别：介于 Java 程序部署平台和主流的容器技术都是基于 Linux 系统，所以这里以 Ubuntu 发行版本作为基础 AOT 编译和运行打包环境。要对 Java 程序进行 AOT 编译的话首先要安装 GraalVM JDK 的发行版本，这个可以去它的官方网站 graalvm.org 上下载和安装，安装步骤和传统基于 OpenJDK 的发行版本步骤一致。在 Linux 中要是使用 GraalVM 提供的 AOT 静态编译功能，首先需要在操作系统中安装一些 C/C++ 编译器的工具链，在 Ubuntu 和其他基于 Debian 的系统中执行下面命令：sudo apt-get install build-essential libz-dev zlib1g-dev其中的 build-essential 是一个用于 C 语言的开发包，包含了 gcc make gdb 和 libc 函数库等很多工具，这些工具和库对于编译 C 和 C++ 程序是必不可少的。libz-dev 和 zlib1g-dev 是用于压缩和解压缩的开发库，这两个软件包提供了开发 zlib 库的头文件和开发工具，允许您在编写 C/C++ 程序时链接和使用 zlib 压缩库，GraalVM 的 AOT 编译依赖于这些工具来构建本地映像。默认情况下，GraalVM 的 AOT 编译在链接时通常使用系统的标准 glibc 库，但在小型嵌入式设备或对内存要求极低的环境中，glibc 并未针对这些场景做出特别优化。相比之下目前有另外一种专有的 musl 标准库实现，musl 被开发出来就是专注在内存受限的设备上表现优异，使用 musl 生产的二进制文件通常比使用 glibc 生产的文件体积更小，所以更推荐直接使用 musl 替代 glibc 去做编译链接优化。在 GraalVM 的 AOT 编译工具中提供自定义选项，允许开发者去自定义使用 musl 去做生成二进制文件优化。部分 Linux 发行版本系统可能没有内置 musl 库需要提前安装，通过源代码安装的方式可以去 musl.libc.org 官方网站下载源代码包编译安装。但这种方式的需要自己手动配置和编译步骤比较繁琐，建议使用下面这种方式，通过别人已经编译好的 x64 位的 x86_64-linux-musl-native 程序，下载压缩包到指定位置就可以使用。基于 musl 编译的前置条件是依赖于 musl 工具链，但在 GraalVM JDK 中并未提供相关的工具链的支持，需要提前下载 x86_64-linux-musl-native 和 zlib 文件，创建一个用于存放 musl 工具链的目录，将其解压到这个目录中，并且将 x86_64-linux-musl-gcc 和 zlib 进行整合编译安装，步骤如下：# 创建一个工具存储目录 mkdir /usr/local/graal-aot-tools # 进入到这个目录 cd /usr/local/graal-aot-tools # 下载 x86_64-linux-musl 工具链 wget http://more.musl.cc/10/x86_64-linux-musl/x86_64-linux-musl-native.tgz # 解压下载到的 musl 工具链包 tar -zxvf x86_64-linux-musl-native.tgz # 复制文件到工作根目录 cp -r x86_64-linux-musl-native/* . # 下载 zlib 依赖 wget https://zlib.net/current/zlib.tar.gz # 解压 zlib 依赖源码包 tar -zxvf zlib.tar.gz通过一条特定 TOOLCHAIN_DIR 环境变量来指定 musl 工具链安装路径，graalvm 的 aot 编译器会通过此变量来找到 x86_64-linux-musl 的位置：export TOOLCHAIN_DIR="/usr/local/graal-aot-tools" export PATH="$TOOLCHAIN_DIR/bin:$PATH" export CC="$TOOLCHAIN_DIR/bin/gcc"最后把 zlib 和 TOOLCHAIN_DIR 整合进行编译安装，进入 zlib 源代码安装目录中执行： ./configure --prefix=$TOOLCHAIN_DIR --static make make install配置完成之后可以通过 x86_64-linux-musl-gcc 命令检查 musl 工具链是否安装成功。如果使用的是 maven 来构建自动化项目，可以使用 pom 文件中配置 AOT 相关构建参数，来自动化来构建二进制文件： <plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> <version>${native.maven.plugin.version}</version> <extensions>true</extensions> <executions> <execution> <id>build-native</id> <goals> <goal>build</goal> </goals> <phase>package</phase> </execution> </executions> <configuration> <skip>false</skip> <imageName>${bin.file.name}</imageName> <mainClass>${app.main.class}</mainClass> <buildArgs> <buildArg>--gc=G1</buildArg> <buildArg>--static</buildArg> <buildArg>--libc=musl</buildArg> <buildArg>--no-fallback</buildArg> <buildArg>-march=compatibility</buildArg> </buildArgs> </configuration> </plugin>在项目的根目录中执行 mvn native:build 命令就可以调用位于 $JAVA_HOME/bin/native-image 工具执行静态分析并且生成二进制可执行文件。AOT 编译对计算机硬件资源的要求非常高，取决于项目的大小，大型项目编译时需要占用很多 GB 的内存和大量的 CPU 使用率，不过这会使用高性能的 CI/CD 服务器来完成自动化构建工作。通过 x86_64-linux-musl-gcc 和 musl 做链接编译生成的二进制可执行文件，完全使用的是静态链接的方式，从而提高了可移植性。在不同的 x86_64 的 Linux 系统上运行这些静态链接的二进制文件时，不需要任何额外的库，这对于部署和分发二进制文件非常有用。从编译日志输出可以看到，AOT 编译器启用了 PGO: ML-inferred 优化，这个默认在其他平台是不会启动的，只要在 Linux 平台下使用 x86_64-linux-musl-gcc 才会开启的，由于使用的完全静态链接的二进制文件，所以可以使用 upx 针对这个二进制文件进行压缩，压缩之后的二进制文件可以小到 3 MB 大小。这对于将其打包为 docker 容器镜像文件非常有利，意味着不需要任何其他基础镜像，直接打包可执行二进制文件放到容器中执行，基于 scratch 基础镜像打包之后镜像大小和二进制文件大小一致。# 使用一个轻量的基础镜像 FROM scratch # 复制静态链接的二进制文件到根目录 COPY target/vasedb / # 设定工作目录，这是可选的 WORKDIR / # 默认的启动命令 CMD ["/vasedb"]采用 AOT 和 JIT 模式通过这些数据，形成鲜明的对比，优势也展示出来了，以往采用 JIT 模式运行的 Java 被容器化之后镜像大小往往都是上百 MB 大小的。个人认为有 Serverless 更激进优化方案，在 AOT 编译之后程序会内嵌入一个微型的 SubstrateVM 运行时，这个运行时负责 Java 程序在运行阶段一些多线程和内存分配、垃圾回收功能，默认 AOT 编译使用的 serial 垃圾回收器是一种单线程垃圾回收器，适用于单线程应用或有少量线程的应用，另外一个可选的垃圾回收器是 G1 垃圾回收器，G1 用于多核处理器和大内存机器的高性能垃圾回收器。但是在 Serverless 应用场景中往往应用程序运行时间很短，在程序在运行一段时间之后就主动销毁了，这有点类似于在 AOT 编译阶段的编译程序本身步骤，当编译器编译完程序就销毁了，是一种一次性应用程序。熟悉 JVM 多个垃圾回收器实现应该知道其中有一个叫 Epsilon 垃圾回收器，该垃圾回收器不会执行任何垃圾回收操作，它只进行内存分配而不释放内存。所以我个人认为在某些特定的 Serverless 应用场景中完全可以使用 Epsilon 内存分配器的方案，因为部分场景程序运行时长还没有等到 GC 回收器触发的时间点就被底层基础设施销毁了，程序运行所分配的内存会被 OS 主动回收，而不需要执行垃圾回收器进行回收，去掉 SubstrateVM 中的垃圾回收器部分，打包应用程序体积会更小。引用资料文件Oracle (The Art of) (Java) Benchmarking PPTAOT vs. JIT Compilation in JavaGraalVM Native Image 快速入门
对于写代码和编写程序来说，放到今天稍微学习一下 Python 和 JavaScript 这类编程语言就可以实现满足自己需求的应用程序，因为已经有很多很好封装好的框架共我们开发者使用，乃至一个中学生都不需要懂一些数学知识，调用相关的 Math 包就可以进行高等数学相关计算。现在有许多高级的编程语言和优秀的框架，使得开发变得更加容易和高效，Python 和 JavaScript 等语言拥有丰富的生态系统和强大的社区支持，为开发者提供了各种各样的工具和框架，使得构建复杂的应用程序变得更加容易。上文提到从需求变成编程再到程序，需求是代码编写者对于软件系统所需功能和特性的描述，例如实现一个四则运算计算器程序，可以是在程序直接编写 console.log("20 + 4 = ",20 + 4) 这种形式来完成需求，也可以是实现一个漂亮的界面完成对应的需求。从 IDEA 到 Coding 到 Product 这个过程中 coding 环节是属于技术实现环节，实现某个功能可以有很多种技术方案，但是方案这么多作为程序员如何选？主流开发的软件都属于应用软件，目前跑在电脑上和跑在手机上的应用软件并没有一些本质区别，都是程序员将自己的大脑逻辑抽象通过代码的形式告诉计算机，当执行这段代码运行之后该程序就会按照你编写的逻辑去交给 CPU 执行，只不过 CPU 要比我们脑子里面计算快得多。这个过程就是 Coding 阶段不能确保程序员大脑里的逻辑一定是正确的，当不合规的逻辑被编写到程序代码中，就可能会产生很严重的后果，例如程序崩溃导致的事件。在电脑和手机只要你开发的不是一些基础的软件，应用软件遇到问题最多就是卡死重启一下，现在的服务器端软件更是搞出来主从或者高可用架构来解决程序员在编码阶段留下的 bug 的问题。在工业时代崇尚机器自动化，现在计算机程序已经渗透到各行各业中了，天上飞的地上跑的水下游的，飞机汽车潜艇这些设备上都靠着计算机程序在辅助人类完成一些操作。机械工程师在设计一些机械结构时犯下的错可能会导致飞机会坠毁，即使是设计合理的机械结构现在控制这些机械装置的已经不是人类了，是跑在计算机中的程序在控制这些机械结构。所以一架飞机或者自动化驾驶的汽车出了事故，责任不完全在机械工程师身上，更有可能的是编写软件的工程师导致的。即现在从一名普通人转行为一名程序员太容易了，普通人通过特定的 IT 培训经过几个月的训练就可以写代码，此种工程师可能不具备专业的 CS 知识就可以动手写程序了，带了问题就上面所说的不可靠软件，如果需要开发专业型的软件还是需要专业型的知识作为铺垫的，例如让一个学土木工程或者做建筑设计的去做软件工程师，那就对应上了中文的俗语 “隔行如隔山” 。一些知名计算机科学界的先驱们就提出了点观点 Dijkstra 曾说过：[scode type="yellow"]我们所使用的工具对我们的思维习惯有着深远的影响，因此也对我们的思维能力有着深远的影响。......使用 COBOL 会削弱人的思维；因此，教授 COBOL 应被视为刑事犯罪。[/scode]简单的工具容易被使用，但是会让使用者不去思考背后的原理，导致缺乏逻辑思维和不去思考事物的本质。这在日常生活中使用的智能手机可以拍照可以打电话可以玩游戏，但是发明和制造一台手机所需要的知识远远不是一个人就能做到的，是一个跨学科和跨领域合作的产品。现在用电脑写一篇文章的时候打错字可以回退修改，但是如果在纸上你写错了一个字可能就报废一张纸，解决办法则你可以使用铅笔进行写字配合着橡皮擦使用，古代人使用毛笔更需要思维和手法练习才发展出来了书法行业。现在已经很少有人去使用了 COBOL 语言进行程序设计了，虽然我本人也是没有写过，但是在研究一些 PL 过程中了解到 COBOL 这门 PL 语言属于结构化加命令式结合产物。但是新的流行语言例如脚本语言和弱语言 JavaScript 和 Python 这些只有在运行的时候才能确定某个变量的类型，还有 C/C++ 这类有空指针设计的 PL ，也会带来底层软件的 Bug ：使用这里语言开发的程序如果程序在开发期没有有效找出 bug 或者程序员的逻辑错误导致 bug 产生，当程序发布之后这些带有 bug 程序会被发布到线上环境，可能在未来的某一时刻会因为这些 bug 导致程序出现故障从而影响到整个系统。一些隐秘的 bug 不一定尽快被修复，例如一个计算闰年和数据类型存储精度丢失的 bug ，要导致触发这个 bug 条件是当前是时间是处于闰年或者闰秒才会发生错误。这类隐秘的 bug 是开发阶段程序员逻辑错误导致的没有处理好，在修复的这些 bug 过程中需要花大量的时间解决，如果在开发这些涉及相关领域知识软件系统时应该提前去查看一些相关领域的资料文件，使用一些业绩主流的解决方案，例如被使用最广工业级时间库，来减少不必要的找 bug 和修复 bug 的时间浪费。现在内存安全的编程语言研究有很多了，但是能作为系统层开发的编程语言屈指可数，除了 C/C++ 这两门语言以外近些年也出现了能替代他们的编程语言例如 Zig 、Rust 这些编程语言，当时目前具备这样的能力程序员还很少，还另外一种解决方案使用静态程序分析产品来提高代码和软件的质量。应用层的内存安全编程语言更不用说，目前主流的 Python 、Java 、C# 、Ruby 这些都属于应用层的编程语言，但是应用层编程语言大部分都是脚本和解释性语言，能作为工业级程序设计的也就只有 Java 语言为主流了。很多人在开始一个新项目之前都会尝试使用新比较时髦技术去开始试错，尝试比较新的编程语言去写新的项目，风险比较大，而且很多解决方案也没有一个主流统一的企业开发标准，例如 Java 是有企业开发标准的。最典型的例子就是 Go 语言，Go 语言在很早之前连一个包管理工具也没有，统一包管理工具也没有了，这就导致每个团队都各自搞各种的开发依赖包解决方案；包括现在依赖包管理工具也不是那么成熟，很多开源的第三方库，从 v1 版本升级到 v2 版本时，是直接弃用掉 v1 方式的 API 设计，导致再次使用新版本时会重新去学习新的 API 使用方式和在源代码里面更改 v1 包路径映射到 v2 版本上，这也是浪费大大时间的。在程序员技术圈里经常流行一些鼓吹某些编程语言和技术方案的案例，实质为其争吵起来。一些编程语言脑残粉那种会鼓吹某个编程语言，当然一些也是抱着相关技术布道的目的地。很多某个语言脑残粉就会盲目的推崇某个语言，在大公司里有足够的人力和财力，开源让一些人去试错，但是试错是有代价的。选择 PL 还是要根据场景来的，例如开发高性能的网络 IM 聊天服务器应用，只要能实现网络通信的 PL 都可以去实现。那如何去选择对应的 PL 呢？但是如果从事迹开发角度来说你考虑内存占用吗？考虑实时性要求吗？考虑性能要求吗？还是考虑团队综合技术水平和开发效率时间快慢问题？不要试图去说服和你观点不一致的人，因为很多人它的认知和所需要的领域知识不及你全面，而且讨论问题的你还要帮助他去科普某些知识细节，交流起来很费劲。某些脑残粉给予给你扣上一个帽子，某些情况下一部分人就是故意在讨论中参入一些不相关的话题扯开话题或者故意添加干扰原本话题源的质量；参入讨论的前提跟你是一个层级的人或者说是有具备相关领域知识的人，如果观点不一致的双方可以互相尊重交流和倾听求同存异的面对观点进行交流沟通。能够正常沟通前提是双方获取的信息来源都是相同一致的，很多系统在设计阶段采用的方案因为信息不对称信息获取问题，导致此用一些不是主流解决方案和技术栈，这会导致自己在某些新技术栈下走其他成熟技术栈道老路，出现了问题和 bug 也找不到官方的文档和相关的问题解决方案。那如何选择技术栈呢？我一直坚持一个观点就是从源头获取信息，例如通过 Google 搜索统计和当下流行技术栈，还有招聘网站上 JD 大多数公司所使用的技术栈来判断。例如使用 spring 框架可以去 spring 官方技术博客和文档上获取，而不是在互联网上看一些其他人已经生产的二手文章和二手信息。通过数据来说话，主流的技术用什么，解决了什么样的问题，以前有没有这样的方案，二者对比弊端呢，再配合自己应用场景做选择。在中国大陆因为一些特殊原因不能正常访问国际互联网，这也是程序员获取信息源的阻碍，可以尝试着在 GitHub 和 Twitter 找到相关领域专家进行交流，一般这些技术框架或者开源软件都会提供相关负责人的邮箱地址，通过此种方式就能和一线的技术专家交流了。很多国内程序员喜欢通过技术图书来学习国外新的技术，其实很多一部分技术都提供了官方网站和官方文档，通过官方资料去获取信息是最为正确的方式，但是弊端是官方的文档只是介绍如何使用这个技术和为什么要这个技术，还有一些使用此技术优势。但是对应真正开始尝试使用这些技术时，又不知道如何和现有系统做整合。此时可能就会在互联网上查别人的资料和购买技术图书，我不推荐国内的技术图书，很多都是将官方文档抄袭就出版了。我个人比较推荐使用所使用技术栈方案对官方出版的图书进行学习，因为这是官方团队成员所写的，没有谁能比这项技术的作者更为了解这项技术本身，所以推荐找到学习源头获取技术知识。
随着就业形势不佳，互联网科技公司的寒冬来临，越来越多的程序员被裁员。在这样的背景下，越来越多的程序员开始尝试成为独立开发者（indie Hacker），希望通过开发一款产品来实现财务和工作时间上的自由。他们致力于通过开发和推广自己的产品或服务来实现个人或团队的利润和成功。相比之下，独立开发者在产品开发和运营方面可能相对弱势。独立开发者往往只有一个人，大多数是程序员出身，没有在公司里面一个团队的支持，自己需要去发觉需求，对需求进行分析和产品 UI 设计直到通过代码实现该产品，最后部署上线运营，在这个过程中需要很多资源的支持，这篇博文我将介绍一些关于从小白如何转型为独立开发者经验。编码技能当从一名产品经理和 UI 设计师转型到独立开发者，可能已经熟悉产品开发的流程和方法论，但是缺乏专业的编码技能。产品经理通常需要与程序员合作才能完成产品，但当你想作为一名独立开发者时必须要掌握编程这门技能了，或者找程序员外包的项目，但是我不太推荐这样去做，都不是自己用代码实现出来的产品怎么能称之为独立开发者呢。我建议你最好先学习一门编程语言，因为编程可以帮助你将想法变为现实中的产品，在这方面我建议学习 JavaScript 语言，因为它可以用于开发 Web、Desktop App 和 Mobile App 应用。首先学习最新的 ES6 标准 JavaScript 基础，再学习前端 MVVM 类型的 UI 框架，这里推荐 Vue.js 和 React 框架，此外如果需要将产品开发为 Mobile Native App 产品，要考虑多个手机端的同步开发工作，在过去可能需要两个团队才能完成工作，当现如今已经有很多的第三方框架可以帮助实现此需求，此时推荐你学习 ionic Framework ，ionic 可以允许你使用 Vue 和 React 技术来实现 App 程序，并且已经提供了各种手机设备上的 UI 组件来使用，可以省去一些设计 UI 的时间。对于如果产品考虑多个桌面端推荐考虑使用 Electron ，可以使用 JS + HTML + CSS 来完成传统 QT 的桌面端开发工作，因此推荐要学习编码技能转行的朋友，去学习 JavaScript 技术栈是不二之选。从 IDEA 到产品做独立开发者从来都不是一件轻松的事情，除了要应对前期持续的时间投入和打造产品的费用都投入亏损，想法太多，过于追求完美，很多想要的功能根本无法在短期内实现，最后在慢慢实现的煎熬中放弃了整个产品；第二辛苦做的产品真正上线之后却不被市场认可，数据没有起色，从而失去了继续更新的力量。作为一名程序员出生的开发者，把自己的需求变为一款产品，要经历多个阶段，从 UI 和 UX 原型图再到开发阶段实现，最终发布产品上线这个周期很长，且需要的资源支持非常多，面临着很多问题。首先从产品选型出发是做 App 形式的产品还是基于 Web 的 Saas 形式的产？取决于多种因素，包括您的产品定位、目标用户群、技术栈、团队能力、市场竞争情况等等。如果第一款产品，只是作为一次独立开发者尝试，建议还是采用 App 的方式只需要将应用提交到应用商店（如 App Store 或 Google Play，当然需要支付一些对应平台的软件数字开发者证书费用），由平台管理应用的分发和更新；采用 App 的方式发布产品不需要购买服务器资源和考虑扩容问题，使用用户客户端本地计算和硬件资源，如果在产品发布并且运营一段时间之后没有盈利，乃至盈利金额都不能抵消掉开发阶段的资金投入，那么问题可能就出现在 IDEA 和产品选型上了，可以考虑暂时停止开发这款产品了，因此工具类的 App 产品就不会需要维护服务器后期更多费用。获得用户和收入有的独立开发者在做开源项目，经营开源社区，并构建自己的影响力；有的人通过分享、出书、写专栏获得收益；有的人则会开发工具类软件，笔记、闹钟、创意工具等等；有的独立开发者自己开发了很多款游戏。
现在注册一个域名和购买一台服务器部署一个 Web 网站非常容易，现在流行的 Serverless 架构更是连服务器都不需要购买，直接在各个云厂商的后台点点鼠标就可以部署一个在线的 Web 程序，虽然相比传统建站的方式简单很多，但是也有考虑一些很基础因素和配置，这篇博文我讲以博客系统为例子讲解如何维护一个 Web 服务程序。现在有很多流行的博客系统，例如静态的 Jekyll、Hugo、Hexo，动态以 PHP 为首的 WordPress 系统，WordPress 有着完善的生态系统是世界上目前二次开发使用最多的 Web 系统，静态的博客可以部署在 Github Pages 和 Vercel 等，直接将编写好的网站源代码上传到免费的空间即可。而对于动态博客系统还说，需要 LNMP 环境的支持才能正常运行，现在也流行直接购买已经部署好的数据库环境和运行环境来跑博客，对于动态博客系统还是数据才是最重要的，这篇博客将介绍一些方法一名合格的站长该如何运维自己博客。基础防御[scode type="yellow"]博客系统存放在主机之上通过互联网提供世界各地访问，这些访客里面有普通人也有一些黑客，黑客可以利用博客程序的漏洞例如 XSS 和 SQL 注入漏洞来破坏程序，这对于博客系统安全有了新的要求。普通的静态博客完全可以不用管这些，但是对于 PHP 这类的动态脚本语言来说更需要关注，因为 PHP 是一门动态语言如果被黑客注入了恶意代码会导致程序数据泄露，严重的会破坏程序的运行，目前场景的作为就是在博客核心代码里面对用户输入的数据进行过滤或者做合法性校验，或者在 PHP 使用 disable_functions 配置项来禁用一些敏感函数，来保证程序安全性。[/scode]如果考虑不是在应用程序嵌入代码来做防御，可以使用一些带有代码注入攻击检查防御的网关产品，博客作为一种 Web 程序提供的服务是要经过 HTTP 网络请求的，可以考虑采用带有防御型的网关产品。例如 Cloudflare 它可以提供基于 SASE 和 SSE 平台，将网站的域名的 DNS 接入到 Cloudflare 并且使用它来解析服务网站，可以改善安全和韧性减少攻击参数传入到主程序中，国内也有长亭科技的 SafeLine 的 WAF 产品。Cloudflare 并且提供 CDN 服务，可以隐藏真实的主机 IP 地址，也能确保域名和 DNS 设置的安全，防止域名劫持和 DNS 污染攻击。对于 HTTP 协议来说才有基于 TLS 安全层的 HTTP 协议，启用 SSL/TLS 证书，确保所有数据在传输过程中都是加密的，防止 Cookie 和 Session 信息被截取或篡改。对于基于使用 LNMP 作为基础设施运行的系统，要考虑是不是使用的长期支持版本的软件，如果使用的不是长期支持的版本的软件，如果出现了漏洞，对于这些已经不维护的基础软件会很麻烦，不能得到相应的补丁更新。操作系统环境还是建议使用基于 RHEL 系列的 Linux 发行版本，现在 CentOS 已经是最优的，我推荐使用 Rocky Linux 这个系统是 CentOS 创始人创建的一个克隆 RHEL 的开源操作系统，是在 RHEL 的基础上进行重新编译的，该系统在很大程度上兼容 RHEL 版本的，并且可以通过相同的方式安装 RHEL 软件包，还能得到长期的更新支持。当整个基础环境搭建完成之后，强烈不建议使用 root 账号去运行网站的基础设施，更应该创建一个专用的用户组和用户来运行对于的软件，这样即使黑客绕过前面的基层防护，也没有更高的权限去访问其他的数据和程序。服务器防火墙软件还是推荐使用目前主流的 firewalld ，这是 REHL 企业级的服务器管理软件，相比一些老的 iptables 来管理出入站流量复杂的操作更容易操作。不要开放不必要的端口，一个 Web 服务器最多开发基础的端口 80 、 443 、22 端口，其他端口可以一律禁止外部访问使用，最好的方式使用白名单 IP 的方式进行访问控制，通过跳板机进行管理。对于 root 用户来说应该不使用 root 密码进登录，而使用基于 SSH 的非对称密钥进行登录，可以将登录电脑和被管理的服务器进行绑定，将公钥上传到远程服务器来进行登录管理。此外此种方式还要考虑私钥的安全性，不要将私钥公开，避免在页面或日志中输出敏感信息，如数据库凭据、API 密钥等。数据备份在线系统的核心是数据，这些数据一般会被存储在 MySQL 这些关系型数据库中，目前主流的动态博客系统数据都是存储在关系数据库中，前面提到黑客通过一些特殊的手段获取到了服务器执行权限，从而可能会导致数据库被拖库导致数据泄漏。对于数据库系统来说管理也是不推荐开放 root 权限给远程用户登陆，对于 Web 程序所依赖的数据库表所在数据库提供对于数据库账户来管理，并且取该账户的一些敏感权限例如删除数据表，添加新字段等，最后 Web 程序的 IP 和能被访问数据库 IP 白名单进行绑定，这样以免泄露了账户和密码也能阻挡恶意的用户远程连接到数据库。建议在数据库中的敏感字段进行加密存储，并且定期对数据库的数据进行周期性备份。对于普通博客系统建议每天执行一次数据库备份计划，例如使用 crontab 进行周期性执行备份脚本，这个备份可以使用 mysqldump 这类工具部分，对其备份的数据可以考虑加密存储。前面所说的是传统备份，部分云厂商提供了主机快照功能，可以对整个主机的数据进行快照备份储存，当然要额外支付一些费用，不管以何种方式进行数据备份，最重要的是养成备份数据习惯，即使数据丢失也能通过备份数据恢复。
开发基于网络的程序时，有许多因素需要考虑，以确保你的应用程序是高效、安全、可伸缩和用户友好的。但是今天这篇文章更加关注的是 OSI 模型中的应用层协议和运输层的协议，基于这些层的程序开发更加关注什么？如果作为一名应用开发者我们最多接触到的网络层面上的应用协议和运输协议；应用的概念更加面向用户，强调的是提供某种服务或功能，可以是桌面应用、移动应用、Web 应用等，这里围绕的是应用开发如何对网络协议选型，因为应用开发是在操作系统用户态上的开发，对于内核的可编程操作是不允许的，所以本篇文章更加关注的是基于 TCP/UDP 之上的协议选型时要考虑的问题。TCP 和 UDPTCP 和 UDP 都是运输层提供两套协议，提供系统内核的 API 可以使用它进行运输数据包达到另外一台主机上，市面上很多的远程调用框架例如 gRPC 和 Thrift 在底层运输数据包都是采用的内核提供的 API 进行发包的。这些 RPC 框架在应用层之上提供了一种结构化的、高效的远程调用机制，使开发者能够更轻松地构建分布式系统。虽然它们通常在应用层协议的基础上构建，但它们自身更类似于框架或库，提供了更高级别的抽象，用于处理远程服务调用的复杂性。gRPC 使用 Protocol Buffers（ProtoBuf）作为接口定义语言（IDL），这允许开发者定义服务接口、消息格式等，而不用关心底层通信细节。这里围绕的 gRPC 来讨论这个问题，了解 gRPC 协议都知道该协议是基于 HTTP/2 协议，利用了 HTTP/2 的多路复用、头部压缩、二进制帧提供了更高效的网络传输，相比 HTTP/1.1 使用多个 TCP 连接，每个连接只能传输一个请求和响应，这可能导致 TCP 连接的浪费和性能瓶颈，而 HTTP/2 引入了多路复用的特性，允许多个请求和响应在同一 TCP 连接上并行传输，这样也减少了 TCP 连接的数量提高了性能。[scode type="yellow"]通过这么一解释就很容易明白 gRPC 为什么要使用 HTTP/2 协议作为基础的应用数据传输协议了，HTTP/2 协议属于是应用层的协议，它依赖运输层协议是 TCP 协议，确实符合 gRPC 协议的应用场景，针对在分布式系统上对数据的可靠性和高效的数据传输要求。也从侧面角度反映出了为什么 Spring Cloud 没有直接使用 gRPC 这种协议进行数据通信，而是直接使用 HTTP 协议，随着 HTTP 协议版本改进采用更高效的二进制数据传输，性能和 gRPC 也不相上下。Spring Cloud 并没有直接使用 HTTP/2.0 进行通信，而是建立在 HTTP 协议之上。默认情况下 Spring Cloud 使用 HTTP/1.1 作为通信协议，但是可以通过一些配置和定制来启用 HTTP/2.0 ，相比 gRPC 特殊的数据传输格式，HTTP/2.0 反而没有那么多数据格式要求可以自定义是 JSON 还是 XML 这种，最终都是会被编码为二进制数据帧。[/scode]对于需要可靠型网络数据传输的场景，使用 TCP 是一个最佳的选择，但是也并不意味着盲目的去使用它传输数据。 TCP 传输数据之前每次都是需要进行 3 次握手，这 3 个握手数据包是主要的耗时来源，延迟就此产生了，如果在一次性很短暂数据运输需求下，这就显得不是那么适用了，快速打开一个连接使用完成之后快速释放掉，很多应用层的网络程序都是跑在 TCP 协议之上，必须经过这个慢启动的过程。HTTP/1.0 版本主要就是针对此种突发性连接的场景，获取到某个 HTML 内容立马断开 TCP 连接，完全没有利用好 TCP 协议，每次发送一个 HTTP 请求都需要经过一次 TCP 慢启动过程。
众所周知计算机软件的架构目前最为常见的是 B/S 和 C/S 架构，B/S 和 C/S 架构的软件都采用了分层结构，将系统划分为不同的模块层次，每个模块层负责相应功能，这种架构便于管理和维护，同时提高了系统的可扩展性和灵活性。笔者感觉最主要的特点是在使用软件时都会产生数据，而这些数据如果存储在本地时很不安全的，并且用户产生的数据才是开发对应软件公司想用的东西，数据才是一个软件科技公司的财产。两种架构都将用户界面和数据处理分离，有助于系统的灵活性和维护性，用户界面只需要负责展示信息，而数据处理由服务器端或客户端中的逻辑处理模块完成；无论是 B/S 还是 C/S 架构，都涉及客户端和服务器之间的通信，客户端向服务器发送请求，并接收服务器的响应，这种通信机制实现了应用程序的交互和数据交换，这篇博文将探讨一些在 B/S 架构下的网络通信协议技术。Web 浏览器和客户端[scode type="yellow"]笔者将其 B/S 架构归纳为 C/S 架构中，其 C/S 架构的中的 C （client）一般是指的安装在用户电脑上的软件，那么其浏览器也是归于客户端软件的，浏览器本体也是一款软件，只不过它可能比其他客户端的软件实现了多种的应用层的网络通信协议。B/S 架构中，客户端通常是 Web 浏览器，用户通过浏览器访问应用程序，这种架构具有跨平台、易部署和易维护的优势。B/S 和 C/S 架构都可以支持多个用户同时访问系统，多用户的需求可以通过在服务器端处理实现，架构图：[/scode]当浏览器发生数据请求就会采用 HTTP 协议和应用服务器交换数据，而应用服务器和数据库服务器交互则采用传输层的 TCP 协议。HTTP ProtocolHTTP 协议是 HyperText Transfer Protocol 的缩写 (即超文本传输协议），是由 w3c（万维网联盟）制定的一种应用层协议，用来定义浏览器与 Web 服务器之间如何通信以及通信的数据格式。因为 B/S 架构中的通信模块就是以 HTTP 这个协议作为标准协议的，所以对该协议有所了解可以更好的编写程序。 HTTP 协议的发展历史悠久，已经不断的迭代多个版本，不同时期的 HTTP 版本特性，整理表格：时间段HTTP版本主要特点1989-1995HTTP/0.9- 最初版本，仅支持 GET 请求。- 无请求头或状态码，只有响应主体。- 用于传输 HTML 文件。1996-1999HTTP/1.0- 引入多种 HTTP 方法（GET、POST、HEAD 等）。- 引入状态码、请求头、响应头等概念。- 服务器在每次请求后关闭连接。1999-2015HTTP/1.1- 引入持久连接，减少连接建立的开销。- 引入管道化（Pipelining），实现多请求并发。- 引入 Host 头字段，允许多个域名共享 IP 地址。- 引入分块传输编码和压缩，提高效率。2015-至今HTTP/2.0- 完全重新设计，旨在提高性能和效率。- 改进多路复用，支持并行请求和响应。- 引入头部压缩，减少数据传输大小。- 允许服务器推送资源，提高效率。- 强制使用加密（TLS），提高安全性。2020-至今HTTP/3.0- 基于 QUIC 协议，进一步提高性能和安全性。- 改进连接建立速度和稳定性，减少握手延迟。- 实现数据包级别的错误纠正和拥塞控制。- 采用基于 UDP 的传输协议。[scode type="yellow"]HTTP 协议一次请求对应一次连接，当浏览器再次发请求给服务器时，Web 服务器并不知道这就是上次发请求的客户端，这也是 HTTP 协议的一个特点：无状态协议。这种需要时建立连接，使用结束后立即断开连接的方式使得 Web 服务器可以利用有限的连接为尽可能多的客户提供服务，并且能够处理分散的网络请求，处理来自不同地区浏览器请求，并且服务器也可以多台分散在不同地区的，正因为是一种应用层协议还可以使用多层代理架构，例如使用 Nginx 做代理网关，正是具备了这样的特点，才使得 B/S 架构能够承载企业级应用的大量访问。[/scode]如果要测试 HTTP 协议的一些特性这里推荐使用 telnet 命令行工具，可以在命令行进行 HTTP 数据包封装，Telnet 使用了 TCP/IP 协议进行通讯，Telnet 协议默认是 23 端口。它允许用户通过网络连接到远程主机，并在远程系统上执行命令，就像在本地终端上输入命令一样。完全可以使用 telent 命令行工具，来模拟整个 HTTP 协议连接建立的过程，HTTP 请求和响应的过程如下：[scode type="green"]浏览器根据 IP 地址和端口号与服务器建立连接向 Web 服务器发送请求数据包Web 服务器接收请求数据包后，发送相应的响应数据包浏览器接收响应数据后关闭连接[/scode]在 Windows 中的 telent 默认会在高级功能设置中，而 MacOS 和 Linux 用户则需要手动进行安装对应的 telent 命令，下图是一次使用 telent 进行 HTTP/1.1 版本请求响应的例子日志细节。$: telnet www.ibyte.me 80 Trying 172.67.151.15... Connected to www.ibyte.me. Escape character is '^]'. GET / HTTP/1.1 HTTP/1.1 400 Bad Request Server: cloudflare Date: Mon, 09 Oct 2023 11:55:59 GMT Content-Type: text/html Content-Length: 155 Connection: close CF-RAY: - 400 Bad Request 400 Bad Request cloudflare Connection closed by foreign host.[scode type="yellow"]在这个例子使用的 HTTP/1.1 版本，这个版本最大特性是多了 Host 头字段，允许多个域名共享 IP 地址，可以让一台 Web 服务器主机占用 80 端口来服务多个子网站网站，Web 服务器可以通过 Host 头字段区分具体要返回域名的内容，Host 字段是区别于 DNS 的，DNS 只是帮助浏览器通过域名查询到 IP 建立 TCP/IP 的连接再进行 HTTP 协议通讯，请求流程如下图：[/scode][scode type="yellow"]HTTP/1.0 相比前两个版本做了很多改进，Host 字段只是其中一个改进之一，另外为提高性能引入了 HTTP Keep-Alive 机制，老版本在发送 HTTP 数据包之前，每次都需要建立新的 TCP/IP 连接通信，如果读者了解过 TCP/IP 协议如何建立连接的，会明白这个过程很费时，所以 HTTP/1.1 版本引入了持久连接，以减少在每次请求时建立新连接所带来的开销，提高网络通信的效率。Keep-Alive 默认超时机制是 60 秒，也就是说如果连接在连接池中的空闲时间超过 60 秒，则该连接会关闭。[/scode]另外 Keep-Alive 超时间隔时间在各个 Web 服务器配置可以设置，Keep-Alive 机制对应的请求头中的是 Connection 字段，服务器会根据此字断来做相应的处理。字段作用Connection: keep-aliveTCP 持久连接Connection: close要求服务器关闭 TCP 连接[scode type="blue"]HTTP/1.1 为提升连接性能，旧版本的 HTTP/1.0 每发送一次 HTTP 请求都会重新建立一次 TCP 的连接才能正常发送正常的请求数据包，导致的问题如果客户端请求到一个页面里面包含了 5 个图片文件，那么浏览器会继续创建 5 次的 TCP 连接来完成这些 HTTP 对象资源的请求；而另外则为上面提到的 Keep-Alive 机制，持久化连接，多个 HTTP 请求共享一个 TCP 连接。还加一个 Pipelining 机制，允许在发送数据包多个请求一起发送，类似于 Redis 命令中的 Pipelining 机制，批量处理数据包请求，不使用 Pipelining 时，每个请求都需要等待前一个请求的响应返回才能发送下一个请求，这样会导致网络的低效利用。[/scode]下面是笔者一个使用 Telnet 例子日志信息：$: telnet www.ibyte.me 80 Trying 172.67.151.15... Connected to www.ibyte.me. Escape character is '^]'. GET /path1 HTTP/1.1 Host: www.ibyte.me Connection: keep-alive GET /path2 HTTP/1.1 Host: www.ibyte.me Connection: keep-alive GET /path3 HTTP/1.1 Host: www.ibyte.me Connection: keep-alive [scode type="blue"]在 HTTP/2.0 又引入了 Multiplexing 技术来提升性能，因为 Pipelining 存在 Head-of-Line Blocking 问题，这是什么问题？ Head-of-Line Blocking 问题根本原因是假设客户端发送了 3 个请求 A 、B 和 C 到服务器，请求的顺序是 A 、B 、C 。如果服务器处理请求 A 的时间较长，那么请求 B 和 C 就必须等待 A 完成后才能开始处理，响应也必须按照 A 、B 、C 的顺序返回给客户端，即使后续的请求处理时间很短，也需要等待前面的请求完成。[/scode]而 HTTP/2.0 引入了多路复用 Multiplexing 机制，允许客户端同时发送多个 HTTP 请求并在同一个 TCP 连接上进行传输，同时服务器也可以将多个响应并行地返回给客户端，不同请求的数据可以交错传输，而不必等待前面的请求响应完成。这样可以充分利用连接，减少了等待时间，提高了效率，总体来看不同性能提升方案差异对照图。[scode type="yellow"]HTTP/2.0 将每个请求或回应的所有数据包，称为一个数据流（stream），每个数据流都有一个独一无二的编号，数据包发送的时候，都必须标记数据流 ID，用来区分它属于哪个数据流；数据流的编号是递增的，客户端和服务器都可以创建和使用数据流，数据流的编号从 1 开始，分别由客户端和服务器独立维护，客户端发起的数据流使用奇数编号，服务器发起的数据流使用偶数编号。[/scode][scode type="yellow"]此外 HTTP/2.0 为提升整个传输效率问题，针对每次需要重新传递的 HTTP Handler 请求协议头，大部分情况下请求协议头都是固定，除了极少的字断经常变动之外；HTTP/1.1 版本每次传输会增加 500-800 字节的开销，如果使用 Cookie 来存储数据会增加千字节 KB 。而在新的版本 HTTP/2.0 则使用 HPACK 压缩格式压缩请求和响应标头元数据，HPACK 核心就是采用 Static Table Definition 结构来服用存储请求头元数据信息，和 Huffman Coding 编码对数据进行压缩传输。[/scode][scode type="yellow"]HTTP/2.0 协议中的 HPACK 复用机制，协议服务端可以维护一个动态缓存表，在发送新请求时协议会和之前请求做对比，之前传输的值的索引列表让我们可以通过传输索引值对重复值进行编码，索引值可用于高效查找和重建完整的标头键和值，如下图：[/scode]至于对于具体值和索引，建议查阅 IETF RFC7541 规范文档，如果不是自己去开发实现 HTTP/2.0 协议的话。另外仿照 TCP 的流量控制策略，HTTP/2.0 也设计了流量控制策略算法，在同一个 TCP 连接上的多个数据流是共享当前运输层的 TCP 连接的，基于 SETTING 和 WINDOW_UPDATE 帧进行数据窗口的调节，具体如何调节看客户端和服务器端的场景。所谓窗口调节也就是在请求头上双方协商调整滑动窗口的大小，这是一段 Java 代码的例子：import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; public class Http2FlowControlExample { public static void main(String[] args) throws IOException { // 创建 HTTP/2 服务器 HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); // 创建上下文处理器 server.createContext("/", new MyHandler()); // 启动服务器 server.start(); } static class MyHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) throws IOException { // 发送流控制窗口大小为 4096 的响应头 exchange.getResponseHeaders().add("Content-Type", "text/plain"); exchange.getResponseHeaders().add("Window-Size", "4096"); // 获取输出流 OutputStream os = exchange.getResponseBody(); // 模拟发送数据，每次发送 1024 字节 byte[] data = new byte[1024]; for (int i = 0; i < 10; i++) { // 发送数据并检查流控制窗口 os.write(data); os.flush(); // 模拟接收方处理数据 handleData(data); // 发送 WINDOW_UPDATE 帧，增加流控制窗口大小 exchange.getHttpContext().getServer().getExecutor().execute(() -> { exchange.getResponseHeaders().add("Window-Update", "4096"); }); // 暂停 1 秒以模拟处理延迟 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } // 关闭输出流 os.close(); } private void handleData(byte[] data) { // 模拟接收方处理数据 System.out.println("Received data: " + new String(data)); } } }正常情况下，流量控制的处理应该由 HTTP/2 协议自动进行，而不需要手动添加 Window-Update 这样的字段，而发送 WINDOW_UPDATE 帧是由 HTTP/2 实现自动处理的。而不是通过添加响应头的方式，HTTP/2 协议会自动进行流量控制，确保在不超过对端流控制窗口大小的情况下发送数据。针对 HTTP/1.1 主动请求资源的方式，HTTP/2.0 升级了主动推送服务，当服务器决定要推送一些资源给客户端时，如资源的 URL 、头部信息等。客户端可以选择接受或拒绝这个推送，如果客户端接受了推送，那么服务器就会在接下来发送相应的数据帧，而不需要等待客户端发起请求，在经典的 Web 服务器 Nginx 中可以使用 http2_push 指令来实现这类的推送功能：server { # 启用 HTTP/2 协议版本 listen 443 ssl http2; server_name example.com; ssl_certificate /path/to/your/certificate.crt; ssl_certificate_key /path/to/your/private_key.key; # 启用 HTTP/2 服务器推送 http2_push_preload on; location / { # 设置推送的资源 http2_push /styles.css; http2_push /script.js; # 处理正常的请求 try_files $uri $uri/ =404; } } [scode type="green"]整个的 HTTP/2.0 协议的设计可以认为借鉴 TCP 协议的设计，相比 HTTP/1.1 协议采用基于 TCP 多连接的进行数据传输，而 HTTP/2.0 总体将协议分为了 3 个部分：流 、消息 、帧 部分，采用 TCP 单连接多数据流的方式进行交叉传输，流可以认为是一种针对单个 TCP 连接的做平分的方式，多个流在一个 TCP 连接中传输形成一个虚拟信道。消息则是在流中传输的请求和响应数据包，帧则是消息中的数据包实体，多个帧可以组成一个消息数据包，帧是最小的通信单位，为了提升性能又添加了服务器端推送事件，而针对数据共享 TCP 连接又添加了流量控制窗口，针对反复传输的 HTTP 请求头 Handler 做了首部压缩，客户端和服务器端各自维护来一张动态缓存表 “首部表” 来跟踪已经被存储的请求头字断，不再通过每次请求和响应发送。[/scode]Ajax 异步 HTTP通过上面篇幅已经知道 HTTP 只能适用于从客户端主动发起，而服务器只能被动提供服务，对请求做出对应的请求数据响应；当然这里在 HTTP/2.0 允许服务器推送资源，提高效率，HTTP/2.0 会根据客户端请求主动推送对应的数据包到客户端中。在这种架构下客户端和客户端之间不是直接 P2P 方式进行通信的，而是通过服务器进行通信交互数据。典型的全双工通信例子有股票市场的 K 图，或者一些在线的 IM 即时通讯应用，目前 Web 实现这类应用的方案有通过 HTTP 协议进行请求轮训的方式，或者 WebSocket 的方式，这也是本篇技术博客要详细讲解的技术。[scode type="blue"]Ajax 轮询方式，所谓的轮询请求数据也就是异步的向服务器端定时发送 HTTP 请求来获取最新的数据，但是缺点也是明显的这种方式数据不能及时显示到客户端，轮询的频率只能由客户端来控制。因为 HTTP 协议书是基于 DNS 和 TCP 协议来传输数据，所以如果每次使用 HTTP 进行轮询请求，那么网络延迟会很大，由于发送 HTTP 请求要先查询 DNS 和建立一次 TCP 连接，建立 TCP 连接需要交换控制信息和三次握手。如果基于 Ajax 轮询方式采用的是 HTTP/1.0 版本性能更慢，因为每次轮询一次需要建立一次 TCP 连接，导致会出现 TCP 连接慢启动和 RTT 延迟。[/scode]在使用基于 HTTP/1.x 版本进行 Ajax 轮询的时候会出现一个问题，每次轮询都需要建立一次 TCP 连接，了解 TCP 协议的都明白每次建立一次 TCP 协议连接代价很大，都需要进行三次握手。为了解决这个问题开发者又提出了长轮询的概念 （Long Polling），普通轮询在服务器接收到请求后，开始处理如果有新的数据可用，服务器立即将数据返回给客户端，没有也会立即断开连接；而长轮询的做法，如果服务器暂时没有新的数据可用，服务器和客户端会保持连接打开，等待直到有新的数据为止，或者加一些策略在一定时间后关闭连接做超时控制，下面是一段 Java 实现的代码：import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @WebServlet("/polling") public class PollingServlet extends HttpServlet { private static BlockingQueue<String> dataQueue = new LinkedBlockingQueue<>(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { // 设置响应类型为纯文本 response.setContentType("text/plain;charset=UTF-8"); // 设置响应缓冲区不缓存数据 response.setBufferSize(0); // 设置响应字符编码 response.setCharacterEncoding("UTF-8"); // 从队列中取出数据，如果队列为空，则等待一定时间 String data = dataQueue.poll(10, TimeUnit.SECONDS); // 没有新数据，返回适当的响应 response.getWriter().write(data != null ? data : "No new data"); } catch (InterruptedException e) { e.printStackTrace(); // 处理异常，例如返回错误信息 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error"); } } // 此方法可以在其他地方先调用，例如其他的 Servlet 方法 private static void startDataProducer() { // 启动数据产生者线程 Thread producerThread = new Thread(() -> { try { int count = 1; while (true) { String data = "Data-" + count++; dataQueue.put(data); // 模拟数据每两秒产生一次 Thread.sleep(2000); } } catch (InterruptedException e) { e.printStackTrace(); } }); producerThread.start(); } }对于客户端只需要在收到消息之后继续发送一个 XHR 请求到服务器，JavaScript 代码：function longPolling() { var xhr = new XMLHttpRequest(); xhr.open('GET', 'https://example.com/longpoll', true); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status == 200) { var responseData = JSON.parse(xhr.responseText); // 处理从服务器返回的数据 longPolling(); // 再次发起长轮询请求 } else { // 处理错误，重新发起长轮询请求 longPolling(); } } }; xhr.send(); } longPolling(); // 初始调用 [scode type="yellow"]对于采用轮询的方式采用那个版本的 HTTP 协议进行数据交换需要考虑。另外一个缺点也是很明显的，如何对轮询的请求间隔时间做选择？因为是采用的定时轮询的方式导致客户端和服务器端存在一定同步延迟，如果碰巧服务器消息在客户端轮询请求之前产生，那么客户端和服务器消息同步延迟时间是最短的，只存在请求网络的延迟。当如果客户端请求刚刚返回，服务器就产生了消息，那么此时两端同步的延迟就是最大，长轮询的缺点也是很明显的如果服务器产生的新数据频率很快，对于客户端来说长轮询能及时获取到最新数据，但是意义不大不如设置一定间隔时间去获取这些数据，采用常规轮询的方式进行。[/scode]SSE 事件推送HTTP 和采用 HTTP 轮询的方式都是为了解决，服务器和客户端之间的数据交换，轮询又是基于 HTTP 协议基础之上一种技术应用的实现，目的是让及时获取到服务器产生的数据变更，客户端能获取得到。这两种方式的都是采用客户端主动发送 HTTP 协议来获取数据，遇到的问题也很明显存在消息同步延迟，不能由服务器在产生新的数据同时就将消息同步到客户端中。针对这个轮询延迟交付问题，多浏览器厂商设计一款新的 API 来帮助解决服务器推送消息的问题，采用 Event Source 和 Event Stream 进行服务器推送通知，一个 Go 的实现自定义事件类型和数据推送功能：package main import ( "encoding/json" "fmt" "net/http" "time" ) type CustomEvent struct { Type string `json:"type"` Message string `json:"message"` Timestamp string `json:"timestamp"` } func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // 设置响应头，指定内容类型为 text/event-stream w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") // 设置初始数据 initialData := CustomEvent{ Type: "message", Message: "Initial data", Timestamp: time.Now().Format(time.RFC3339), } sendEvent(w, initialData) // 模拟定时向客户端推送不同类型的事件 ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: // 向客户端发送 "message" 事件 messageEvent := CustomEvent{ Type: "message", Message: "Server time", Timestamp: time.Now().Format(time.RFC3339), } sendEvent(w, messageEvent) // 向客户端发送 "alert" 事件 alertEvent := CustomEvent{ Type: "alert", Message: "Important message", Timestamp: time.Now().Format(time.RFC3339), } sendEvent(w, alertEvent) case <-r.Context().Done(): // 客户端断开连接时关闭循环 fmt.Println("Client disconnected") return } } }) // 启动服务器 fmt.Println("Server listening on :8080") http.ListenAndServe(":8080", nil) } func sendEvent(w http.ResponseWriter, event CustomEvent) { // 序列化事件为 JSON 格式 jsonData, err := json.Marshal(event) if err != nil { fmt.Println("Error encoding JSON:", err) return } // 向客户端发送 JSON 格式的事件 fmt.Fprintf(w, "event: %s\n", event.Type) fmt.Fprintf(w, "data: %s\n\n", string(jsonData)) w.(http.Flusher).Flush() // 强制将数据推送到客户端 } [scode type="yellow"]Server-Sent Events (SSE) 代码实现起来比传统 Ajax 轮询要简单，只需要知道服务器的 SSE 协议地址，就可以发送 SSE 连接，服务器可以发送简单的文本消息到客户端，客户端通过事件监听器捕获这些消息，设置对应的事件处理逻辑即可。 SSE 是基于一个长连接的，相比轮询来推送消息，消息延迟是相对比较低的，但是只需要服务器推送消息到客户端，如果客户端要发送新消息则新建一个 HTTP 连接到服务器进行处理。在正常的 Event Source 使用中，服务器不需要重新建立连接来发送自定义事件，如果服务器和客户端遇到一些特殊情况断开连接了，服务器和客户端双方要考虑维护连接断开之后恢复，此外消息是文本格式同样可以使用 gzip 进行压缩。[/scode]WebSocket 全双工通信与传统的 Web 通信方式不同，WebSocket 允许客户端和服务器之间进行双向通信，双方都可以独立地发送消息。WebSocket 在客户端和服务器之间建立了一个持久连接，这个连接保持打开状态，允许低延迟通信。这样设计的好处允许客户端和服务器之间能交叉发送数据，客户端和服务器双方都可以随时发送消息到给对方。WebSocket 从一个 HTTP 握手开始，然后 HTTP 协议升级到 WebSocket 协议，然后再通过 WebSocket 连接发送的数据被组织成帧，帧可以携带文本或二进制数据，帧的机制支持高效的通信并支持不同的数据类型。因为和上述的通信方式一样，没有固定的消息格式，通信采用二进制数据流的方式，如果需要交互数据，可以在应用和服务器端设计子协议，子协议可以定义应用层的通信规范，规定在 WebSocket 连接上双方如何交换数据、消息和命令。这有助于确保在 WebSocket 连接上进行的通信是按照特定协议的规范进行的。子协议存在的目的统一客户端和服务器端都知道消息首部，各方面采用子协议就能正常编解码消息体，如果双方在握手时都同意使用特定的子协议，那么连接将在该子协议下进行通信，否则连接将使用默认的 WebSocket 协议。[scode type="blue"]网络延迟是必然存在的，数据包要从一台电脑到另外一台电脑必须经过 5 层网络模型，在这些协议栈上封包发包都需要时间，链路上传输取决于传输介质，传播延迟 + 传输延迟 + 处理延迟 + 排队延迟 = 整体网络延迟。这些延迟属于 ISO 网络层面的延迟，没有算上应用程序处理的延迟，和使用场景问题。何时使用 XHR ？如果你请求有突发性和短暂性这时应该使用 XHR 进行请求；如果服务器和客户端需要一些消息通知场景不涉及到大文件传输，可以考虑使用 SSE 进行；如果应用和服务器需要实时同步建议使用时久化连接的通信，WebSocket 可以提供此种方案，只需要首次建立连接握手，传输而使用二进制流方式进行高效的传输，并且可以使用子协议对通信协议进行扩展。[/scode]其他资料计算机网络：自顶向下方法（原书第8版)HTTP 资源与规范反模式：停用 HTTP 持久性HTTP keep-alive 二三事High Performance Browser NetworkinGoogle Web.dev HTTP/2 简介
未来 PWA 应用是否成为主流应用开发模式
PWA 这个词已经出现很久了，如果读者做嵌入式开发应该不会很了解，如果是做服务端和前端或多或少了解，最近在聊这方面的话题发现还是有一些人不知道这个 PWA 是什么技术？PWA 是 Progressive Web Apps 的缩写，中文翻译为渐进式网络应用，早在 2014 年，W3C 就公布过 Service Worker 的相关草案，Service Worker 可以不干扰视图渲染层线程，运行独立于 Web 站点只请求原始数据，而不涉及任何样式或布局信息，但是其在生产环境被 Chrome 支持是在 2015 年，由 Google 工程师 Alex Russell 和 Frances Berriman 共同提出，在他们的一篇文章Progressive Web Apps: Escaping Tabs Without Losing Our Soul 的博文中详细阐述了 PWA 的概念和优势，PWA 旨在将 Web 应用程序与本地应用程序的最佳特性结合起来，提供一种更好的用户体验，包括快速加载、离线访问和与设备功能的集成。[scode type="yellow"]PWA 优点不需要从应用商店进行安装，而是通过网址 URL 来进行访问，简单的来说就是不用下载，用一个 URL 就能直接打开，将一个传统的网页可以渐进式地变成和本地原生 App 一样来使用，比如被添加到主屏幕、全屏方式运行、离线工作、推送通知消息等，这个和目前腾讯的微信小程序类似的。微信小程序确定是被腾讯锁死在微信里面，只能通过微信的小程序商店和微信内搜索得到，但 PWA 你应用可以被搜索引擎直接搜索得到，搜索引擎就为应用到入口，这和各手机厂商的应用商店是有着明显的差异的。小程序和 PWA 这种应用开发模式出现比较晚，因为首次打开这种应用需要从服务器端下载各种资源文件，在 2015 年时候如果读者有使用过手机都清楚那个时候大部分还是 3G 和 4G 的时代，部分地区 WIFI 也不是全面的覆盖，这就会带来一个新问题，如果全部采用 Web App 的方式进行开发应用，每次都需要加载。而采用传统的原生方式开发者只需要把应用上传到应用商店，用户去应用商店下载得到一个固定资源的 App 安装到手机上即可，最后动态数据从服务器端通过 HTTP 协议进行交互，减少一部分常用资源的加载过程，但是放到今天 5G 已经慢慢普及，网速越来越快，用户手机几乎不会切断流量或者 WIFI 连接的，所以个人感觉 PWA 更适合现在的应用开发模式。[/scode]Native App在介绍原生应用之前，我想我有必要介绍一下什么 Web2.0 结构互联网，Web2.0 也不仅仅限制于在浏览器交互数据上，有了 HTTP 协议和移动互联网，更多是指基于 Web2.0 异步数据交换方式。Web1.0 是互联网的早期阶段人们上网的方式是查看一些固定死的静态网页内容，网站维护者每更新一次网站就需要重新发布一个新的网页版本，而且缺乏和用户互动性。Web2.0 是互联网的现代阶段，大约从 2004 年开始至今，在 Web2.0 时代，互联网变得更加互动和社交，用户可以更积极地参与和贡献内容，用户可以把自己的数据通过 HTTP 协议发送到远端服务器上存储，其他用户可以查看，用户之间可以互动，鼓励用户生成内容用户可以创建、编辑和共享各种形式的内容，如博客、社交媒体帖子、视频、评论等。此后移动互联网来了，更多开发者从事开发移动端 App ，而这些 App 和服务器端数据交换大部分也是采用的 HTTP 协议，不管你是使用 Java 还是 Kotlin 还是现在推荐的 Dart 和 Flutter 开发 App，乃至是 Swift 和 SwiftUI 这种，都是属于视图层的开发，最终数据还是存储在远端服务器之上。上图为一个原生的 App 应用架构，开发者如果是 Apple 开发者需要在 App Store 申请和购买开发者证书，才能把自己开发应用上传到应用商店中，而且还要自己维护复杂 App 服务器端和 App Store 中的应用版本和介绍信息，这就使得开发者开发一个版本 App 需要做很多工作，独立开发者更是要自己设计原型图，在通过代码被 UI 图变成用户软件界面，最后如果开发应用不是单机的应用还需要自己接入 API 服务器，这件使得开发周期很长。Progressive Web Apps但是对于现在的 PWA 应用，开发者更关注的实现 PWA 定义的规范和协议即可，就可以将一个网站变成 PWA 应用，当然这里有人会问了 PWA 应用有原生 App 应用性能高吗？或者能和原生 App 一样调用系统 API 实现类似原生应用功能吗？放到过去可能浏览器没有完全支持和提供这方面的 API ，例如你可能要实现一个读取门禁卡的 NFC 功能，那么 PWA 是不支持的，只能使用原生的 Java 去实现；但是今天各个浏览器已经完全提供这方面的 Device API ，提供后台 Service Worker API 允许 service worker 向其 PWA 客户端发送一个消息，还有 Background Synchronization API 运行通过异步的方式执行任务，这些只是列举出来一部分很有很多，具体读者查阅相关的文档。[scode type="blue"]并且 PWA 应用现在不仅仅是只支持移动设备，只要有浏览器引擎支持的，都可以运行，注意这里的浏览器引擎很重要！至于为什么我会在后面的篇幅中解答。下面是一个 PWA 应用不经可以在手机上运行和在桌面端运行效果图：[/scode]下面为桌面端应用的效果图，如下图：这一看是不是感觉焕然一新？是不是觉得 PWA 应用未来的潜力很大，但是问题是如何解决相比原生应用的性能问题呢？PWA 性能问题聊到性能问题，这个就遇到我熟悉的领域了，如果大家都稍微做过一点应用端开发或者前端开发的，都会明白浏览器是一个不可或缺的基础软件，或者你是写 Java ，那么 Java 也是离不开 JVM 运行时支持的，如果你是一个大牛读到这里你应该明白我要说什么了。对的没错，这些技术栈的特点就是需要运行时或者执行引擎的支持。浏览器中执行引擎和 JVM 设计有类似之处，都属于编程语言虚拟机范畴了，Chrome 中用的是 V8 ，而 Dart 语言也是虚拟机语言也有自己的虚拟机，Kotlin 也是一样的，Koltin 官方乃至都可以通过 K2 编译器将代码编译为 js ，放到浏览器上运行，这些的背后都依靠着虚拟机和执行引擎；例如我比较推崇的 GraalVM 项目，它的应用还是在 Serverless 领域，这个后面我也会把它和 PWA 联系起来，因为 PWA 需要后端支持，例如现在大部分云厂商推荐的 Function as a Service 方式来编写服务程序降低成本。[scode type="yellow"]和原生的性能差的问题如何解决？这里的答案是采用 WebAssembly 的方式进行优化！Wasm 是一个低级编程语言，WebAssembly 是便携式的抽象语法树，被设计来提供比 JavaScript 更快速的编译及执行，WebAssembly 将让开发者能运用自己熟悉的编程语言编译，再借虚拟机引擎在浏览器内执行。这里典型的应用例子是 Google 地球，Google 地球最初使用 C++ 语言在 Windows 平台上开发，后来移植到了 Android 和 iOS 平台中，起到关键技术突破是使用了 Wasm 技术，所以未来如果 PWA 应用性能有问题，完全可以将一些逻辑使用 Wasm 来执行，下图为 Google 地球的截图：[/scode]只要能成功连接到服务器加载效果资料，客户端没有丝毫卡顿状况。PWA 是否能成为主流？最近几天如果紧跟时事的话，很多开发者肯定会了解到，在中国大陆地区所有的移动应用和小程序、轻应用都需要进行备案，如果没有备案连应用后台服务器都租不到，并且很多应用使用的第三方厂商的推广服务和统计服务（例如友盟统计），广告服务等，都不能被正常接入。服务端备案对应有自己的个人网站或者博客的，朋友应该很熟悉了，服务器备案已经实行很久了，现在大陆地区 App 全部要备案了，不备案就算是个人开发者都不能上架 app 到应用商店，在大陆做一个开发者是真的难 ☹️ ...我个人还是认为未来 PWA 应用还是会成为主流的，因为 PWA 本质上还是通过网站进行提供服务的，对应 PWA 应用的开发者来说只需要针对网站备案即可，并且不用考虑多个平台的支持，并且目前浏览器已经提供 WebSQL 、IndexedDB 、LocalStorage 本地存储支持，一次加载所需要的资源被缓存到本地，如下图：另外至关重要的是 PWA 程序应用资源文件和服务端 API 如何做？前面的提到了各个 Cloud 云厂商都提供了 CDN 和 Function as a Service 服务，这就可以让开发者把自己的程序文件放到 CDN 上分发到全球，另外配合 Faas 就可以实现一个完整应用程序和服务端服务，架构如下图：[scode type="yellow"]如果最近注意到目前开发者工具 IDE 动态话，与昨天大陆地区 App 需要备案法颁发的同时，8月 8 日 Google 也推出他们新项目 Project IDX 一个基于 AI 的浏览器开发环境，集成 AI 支持全栈编程语言，跨平台真机预览一键部署，真的云端开发模式，IDX 和 PWA 有什么关系吗？看似表面是没有关系的，但是在我看来背后实质上背后在推动 PWA 这种开发模式，Project IDX 工作空间都具有基于 Linux 的虚拟机的全部功能，配合云中托管的通用访问权限，从 GitHub 导入现有项目，让你继续之前的进度。还可以创建新项目，预先包含流行框架的模板，包括 Angular、Flutter、Next.js、React、Svelte、Vue 和如 JavaScript、Dart 、 Java 等语言，在最后，将应用推向生产方面的一个常见痛点是部署它，通过集成 Firebase Hosting，通过几次点击就可以部署你的 Web 应用的可共享预览，或通过快速、安全和全球托管平台部署到生产环境。这里的存储空间和函数服务全部都是在 GCP 上的，个人感觉最终目的就是要把程序代码和数据锁定在 GCP 上，开发运维和应用数据都在 GCP 上完成，强依赖关系按照这个项目想法。[/scode]目前 Project IDX 还没有开放使用，处于预约阶段，以上是我笔者个人对 PWA 应用和大陆地区要求 App 备案的看法，只代表我本人个人看法，预测未来，或许某个天这种开发模式的真就成为主流，时间会告诉我们答案。其他资料PWA 和 AMP ♥ 中国 (GDD China ’17)渐进式 Web 应用Progressive Web AppsProgressive Web Apps: Escaping Tabs Without Losing Our SoulIntroducing Project IDX
分布式系统对于刚刚入门作研发的程序员还是接触不多，刚刚入行的大部分程序员做的最多工作还是开发单体应用和简单业务操作，遇到最多问题估计也是本地线程并发数据安全问题，和数据状态一致性问题。而往后发展最多把单体应用部署为 HA 主从节点的方式备份架构，采用 nginx 做网络应用层的负载均衡。单体应用 bug 修复往往是万能法重启应用，让应用的状态消失，再恢复到正常状态来解决问题，但是实际上问题还是没有得到解决，除非是一些硬件故障可以跟换硬件，软件本身的问题需要开发者自己修复发布新的版本才能行。分布式系统[scode type="blue"]而分布式系统是由多个独立计算机组成的集合，而大型的软件系统被拆分成了多个部分，部署在网络中计算机中，这些计算机通过网络互相通信协同工作，在设计分布式系统时，需要考虑以下几个问题：[/scode]通信协议：如何让分布式系统中的各个节点进行通信？需要确定适合该系统的通信协议，并确保节点之间的通信稳定可靠。数据一致性：如何确保分布式系统中的所有节点都拥有相同的数据副本？需要选择适合该系统的数据一致性协议，如Paxos或Raft等，并在设计时确保数据的一致性。负载均衡：如何在分布式系统中均衡地分配负载？需要设计适合该系统的负载均衡策略，并确保在节点故障或网络问题时系统依然可用。安全性：如何保护分布式系统中的数据和节点不受攻击？需要在设计时考虑安全问题，并实现适当的安全措施，如加密和身份验证等。可扩展性：如何在需要时扩展分布式系统的规模？需要设计可扩展的系统架构，以便可以添加更多的节点和处理更多的请求。故障恢复：如何在节点故障或网络问题时恢复分布式系统的功能？需要设计故障恢复机制，并确保系统在出现问题时可以快速自动恢复。性能优化：如何优化分布式系统的性能？需要考虑各个节点的性能特点，并尝试优化通信协议、数据一致性协议、负载均衡策略等，以获得最佳的性能表现。[scode type="yellow"]网络和时间是设计分布式系统时必须考虑的关键因素，网络延迟、带宽限制和数据包丢失等问题可能导致节点之间的通信出现问题，从而影响系统的性能和可靠性。时间同步也是分布式系统设计中非常重要的问题，因为节点之间的时钟可能会出现偏差，导致数据一致性和协议的正确性受到影响，因此，在设计分布式系统时，必须考虑网络和时间问题，并采取适当的措施来解决这些问题。目前时间存在于物理世界中，一个系统可能被部署在全球的网络服务器节点中，这些地区的用户时区也不一样，而且是还是多应用进行异步操作的，此时分布式系统设计就不单单是单体系统中加个锁就能解决的问题了。[/scode]扩展性对于单体系统数据的一致性也是一个问题，例如数据库和缓存系统的数据一致性问题，当然缓存系统的数据一致性存在时间窗口的偏差是可以容忍的，但是对于分布式系统的数据一致性要考虑到问题加很多，关系型的数据库的 ACID 已经帮业务层解决了很多问题，而分布式系统特别是针对的分布式数据库，数据被分散在不同网络节点上，如何处理 ACID ？目前很多微服务架构是使用一些数据库中间件代理解决，或者说分库分表，而颠覆性的是 NewSQL 数据库例如 PingCAP 和 Amazon Aurora 分布式关系数据库来解决分布式数据一致性系统。[scode type="yellow"]分布式关系数据库用起来来和传统的 Oracle 数据库是没有区别的，对于业务层是无感知的，但是分布式数据库本身的设计问题是要对 CAP 定律进行权衡的，大部分采用的分区同步复制写入的方式，而这些数据如何保证一致性？总的来说，NewSQL 是一种结合了传统关系型数据库 ACID 的优点，并加入了一些新技术，以提高数据库的性能和可伸缩性。[/scode]为提高 CAP 定律中的 P 容错机制，通常采用的方法是将数据分片复制到多个节点上，以提高数据的可靠性和容错性，同时还需要考虑 CAP 中的 A 可用机制，对节点故障进行监测和自动故障转移处理，保证系统的高可用性，NoSQL 系列例如 Redis 采用的是一致性哈希算法，Facebook Cassandra 也是一致性哈希算法，这种采用 Gossip 协议进行数据同步的，但是数据还是分区存储在当个节点上，遇到 ACID 需求无法将整个集群视为一个整体系统做强一致性数据处理，只能在单个数据节点上做 ACID 处理，并且这种分区有热点数据问题。一致性为了保证分区所有节点数据一致性目前大多数数据库采用的是同步复制，换句话来说往集群中一个节点写入数据其他节点马上会同步到最新的数据，但是由于分布式系统是建立于在不可靠网络之上的，如果出现分布式分区脑裂情况，导致多个分区不能通常通信，此时数据版本该从何处写入，从何处复制到集群中剩下的可以节点中？目前大多数 NewSQL 采用两种分布式算法解决的，Raft 和 Paxos 算法。[scode type="yellow"]Paxos 和 Raft 这两种协议的目标都是在分布式系统中实现一致性，保证分布式系统中的各个节点的数据一致性。这里 Raft 算法在工业级使用的是最多的，它采用领导者选举的方式，保证系统中的多个节点之间的数据一致性。相对于 Paxos 协议，Raft 协议的实现较为简单，而且容错性更好。[/scode][scode type="green"]如何让多个分区中的节点数据保存一致？在不同时间下操作的客户端如何保证一致？时间相对于客户端来说对于自己是相对的，而对于服务器来说是要面临多个客户端时间线的，整个世界都是异步的，如何多个分区保证数据一致性？分区出现了脑裂怎么办？在 Raft 协议中把这些抽象成了 3 个部分，第一个数据采用日志的形式同步，第二个多台服务器之间传输数据听谁的？Raft 采用的是选举机制，从多个节点中选择一个领导节点，第三个容错机制采用多节点之间心跳机制来保证，和我之前设计的 web session 那篇文章的超时机制类似。[/scode]在 Raft 算法通过日志复制来保证系统中各个节点之间的数据一致性，领导者会向其他节点发送日志条目，其他节点会将这些日志条目添加到自己的日志中，并向领导者发送确认消息，当所有节点将日志条目持久化并且发送确认消息之后则认为这条数据状态一致性得到保持，至于实现细节可以查看文末，我列出的 Raft 协议相关文章。不可抗拒因素[scode type="yellow"]分布式系统产生脑裂的原因最多原因就是网络问题，网络层的 IP 协议并不能保证网络数据包传输过程中保证数据包正常无误的传输，会出现丢包、数据包网络延迟、重新发送数据包，这些问题在 TCP 协议得到解决，包括数据包顺序问题也得到解决，但是 TCP 不能消除网络延迟问题。延迟是基于时间的东西，这是不可能得到解决，除非有时间机器，分布式系统脑裂出现原因一部分也是因为数据包延迟达到而产生了，或者请求响应问题产生的。能让程序产生延迟现象的原因有很多，下面 3 种都是归根结底是时间带来的问题：[/scode]网络分区垃圾回收基于时间对于分布式系统网络故障，目前最常见的解决方法为负载均衡、主从复制，这些典型案例为 web 应用服务器通过 nginx 做负载均衡，一台服务器出现了故障之后另外一台服务器还能正常工作；主从架构典型应用为 Redis 哨兵模式和 Linux 网络中所有的多网卡绑定设计，都是用来解决网络故障引起系统故障。[scode type="yellow"]解决上面这种网络故障常见，最常用的办法为心跳检查，轮询的方式检查健康，但是别忘了检测服务器和故障服务器之间也是通过网络连接，这样网络分区问题又回归到本身。[/scode]在开发单体应用的 web 的时候会对 http 请求延迟做测试，如果延迟过长客户端会无法加载出来数据，而这些问题也是数据包在网络中传输延迟导致的。同理在分布式中检查故障的手段也是基于时间和超时控制的，如何设置超时时间窗口又带来一个新的问题？设置过短导致网络频繁，设置过长又导致出现了故障不能立马检查到。[scode type="yellow"]导致相应延长原因也有很多，网络数据包链路延迟，或者服务器程序进程崩溃，进程处理数据堆积压力过大，特别是服务进程已经处于高负荷状态，导致无响应，检测端如何应对这种情况？网络数据拥堵问题和现实生活中道路拥堵一样，一段路程原本能在 d 时间内跑完，而出现拥堵情况则是不确定的，出现网络拥堵情况有：[/scode]交换机故障程序高负载虚拟化环境 CPU 切换资源延迟基于有 GC 的语言出现 STW 情况[scode type="yellow"]并且 TCP 有数据包默认的超时机制来重新发送数据包，如果数据包在交换机中堆积，而 TCP 的超时机制会认为数据包已经丢失，导致数据包重新发送，这样以来本来网络就够拥堵的了，再重新发送数据包会更加堵塞，跟会加深网络的延迟影响。在公有云例如 AWS 中多个物理服务器公用一台交换机的情况，网络的不可抗拒因素又会提高。[/scode]假设现在不同考虑到网络可靠性问题了，数据包能在规定的时间内到达分区节点中，现在剩下的问题为服务器本身和应用程序本身的问题，不同的时区的分区节点如何做时间的协调工作，这种最为常见解决方法是通过时间戳的方式，例如两个客户端同时向分布式数据库写入一条数据，谁会优达到呢？这很难受，取决于服务端如何处理，例如服务器的程序正在做 GC 垃圾回收，导致整个进程出现了 STW 状态，当进程苏醒时时间就会存在偏差了。[scode type="yellow"]基于时间带来的分布式问题，这个研究方向太多了，而且时间戳也有 Long 值不能表示的那一天，这让我想起了，存在看到一个科幻的故事，说一个未来人从未来回到过去，寻找 IBM5100 机器，以此来解决计算机世界中的时间问题。目前分布式数据中心的数据库 Google 采用的 GPS 和原子钟的方式解决，时间同步的问题。[/scode]针对于进程内的 GC 导致程序停止问题，和磁盘 IO 读写导致程序阻塞问题影响的涉及时间偏差问题，目前解决方案是使用无 GC 编程语言，或者对 GC 参数做设置，例如 Java 最新版本采用 ZGC 设计方案，或者使用 Rust 这种特殊内存管理的编程语言进行编写应用，针对分布式常见可以采用滚动停机方法进行 GC 回收，当要进行垃圾回收时把执行任务节点从集群剔除，这也是一种常见的做法。[scode type="yellow"]如果你对实现性要求非常高的场景，例如汽车和飞机的各个传感器，对应时间响应要求要为试试的，那么有 GC 的编程语言就不是很好的选择了，可以使用 C 语言或者现在流行的 Rust 编程语言，RTOS 系统之类软件，例如汽车在发生碰撞的那一刻，安全气囊要即时打开，系统肯定不希望 GC 暂停导致无法及时释放安全气囊导致人生安全的事故。[/scode]不可靠因素太多，怎么确定集群中的数据分区问题？故障检查问题？单靠节点自身来检查是完全不够的，而且网络波动导致分区故障，在同一个地区服务器可能会检查到，其他地区服务器因为网络线路原因导致不能监测到，目前最好办法为法定人数来解决，多个节点对一个节点做仲裁，如果集群中的三分之二节点认为某个节点挂了，那么它就真的挂了，即使它进程正在做 GC 垃圾回收处于假死状态，也等同于从集群中移除废除它的工作职责，目前实现这些算法的常用的为 raft 协议和 quorum 协议，以法定人数来解决这些问题。其他资料Distributed Lock ManagerRaft 一致性算法Raft 协议生命周期变化过程造出时间机器，到底有没有可能？How to do distributed lockingStatic Compilation of Java Applications at Alibaba at Scale