GigaHttpd 设计思想 0.2 版

 

 

GigaHttpd 设计思想



版本:  0.2
提交时间:2009-02-05 (2008-10-15 开始写)
本版作者:鲁义明 (Yiming Lu) lu.yiming.lu@gmail.com
所属:  GigaHttpd 开发文档



* 先说实话

  这一版设计思想重点在如何实现,也就是要做第一个原型出来。跟上一版同样的,这个设计可能彻底不可行。不过没关系,虽然我很无知,但我会继续前进 :)

  这一版的原型包括 GigaHttpd 的代码,以及一个示例网站,可以让很多人(百万数量级)同时在一张纸上画画。


* 前言

  建议在看本文档之前,先看过前一版设计思想,即 0.1 版,可以简单的对整个系统有个初步的认识。

  本文是边想系统如何设计,边写出来的,历时四个月,大的思路没变,但后面小节的设计比刚开始的要详细一些。

  最后几个小节的详细内容不写了,因为影响也不太大,所以只留下了题目,备忘,后续版本再详细考虑。

  本版设计思想对开始写代码应该够用了,所以接下来会开始写代码(当然还有数据结构图、协议流程图以及程序流程图),随着第一个可运行版本的逐步开发,会继续修改完善设计思想,并在适当的时候发布。


* 系统结构总述

  在现在这个阶段(GigaHttpd 还太年轻),GigaHttpd 作为 Linux 的一个内核模块来加载。以后是否会彻底脱离 Linux,发展成一个独立的操作系统,留到后续版本考虑。

  所以 GigaHttpd 是一个内核模块,按照一般的内核模块来编译、加载。本文后续部分把这个模块简称为"GH 模块"。

  GH 模块加载后,首先准备好 GigaHttpd 系统的运行环境,然后启动 GigaHttpd 开始运行。整个启动过程类似 Linux 的启动。具体事务如下:

  (1) 准备内存空间。给 Linux 留下基本够用的内存页面。

  (2) 准备 CPU。给 Linux 留下一个 CPU core。

  (3) 准备网卡驱动,初始化协议栈。给 Linux 留下一个正常网卡,其余的都重新初始化成 GigaHttpd 的数据网卡。

  (4) 定位角色,接收计算任务,如果是整个集群中的第一台计算机,则初始化计算任务,并将自己设定成总协调人。

  (5) 加载静态数据。也就是所有的静态数据,从磁盘上,从复制子网获得,从动态的计算任务产生。

  (6) 加载计算任务。动态的计算任务,类似进程、线程的东西,从磁盘上,从复制子网获得,系统管理员上传。

  (7) 内部数据复制,以支持单个数据的大量并发访问。

  (8) 数据日志与备份。

  关于系统运行的监控与调整,我们可以分阶段实现多种监控方式。具体如下:

  (1) 通过 Linux 监控。这应该是最简单最直接的一种方式,在 Linux 与 GigaHttpd 之间通过内存页面交换数据,然后这些数据通过 /proc 来存取,就可以任意显示、控制 GigaHttpd 的运行状态。

  (2) 通过 web 监控。因为我们要实现的是 HTTP Server,所以 HTTP 协议的支持是最基本的。我们可以开发 GigaHttpd 的监控界面,通过浏览器来管理。这大概会是一个很长的过程。等到可以通过 HTTP 协议管理我们的所有功能的时候,旁边的 Linux 就是可有可无的东西了。呵呵,最近正在重新考虑什么是操作系统,是否以后可以不再支持 POSIX。


* 准备内存空间

  GigaHttpd 自己管理内存,即有自己的页表管理机制,不同于 Linux 内核。因为所有的物理内存页都会在两个系统内部映射,所以可以用一些内存页来交换数据。

  一般情况下,GigaHttpd 内部内存分配的基本单位是页。整页分配,整页回收。这个主要考虑我们的目标,是一个 HTTP Server,绝大多数数据内容都是静态存放在内存中,没有频繁的内存申请与释放。

  GH 模块加载后,取得系统中总物理内存页面数,减去给 Linux 留下的内存页面数,就是 GigaHttpd 的总内存页面数。

  GH 模块首先从 Linux 申请一些页面,用来构建页目录和页表。这些目录和页表准备好后投入应用,GigaHttpd 进入自己的内存空间。

  因为是 64 位的系统,所以我们有足够大的内存地址空间。

  GH 模块的代码,被映射到与 Linux 相同的地址空间,保证程序代码可以继续运行。但是所有 Linux 的内核函数,都不能调用了,所以对于基本的 C 库函数,在编译GH 模块时,要静态连接,同时要确保这些静态函数内部没有动态申请内存的代码。如果实在绕不过去,就写一个简单的动态申请内存的"壳"函数,这个函数在 GigaHttpd 内部真正实现。我们不准备重写所有的东西 :)

  多核系统的内存总线、北桥可能会是瓶颈,关于这个等待硬件厂商的升级吧。


* 准备 CPU

  本文所述的 CPU,仅指 CPU Core(下同)。

  为了性能以及系统结构简化,GigaHttpd 内部不进行 CPU 调度。是否将来进行 CPU 调度,后续版本考虑。

  所谓调度,是指一个 CPU 可以在多个进程、线程,或者计算任务之间进行切换,例如把不忙的进程、线程或计算任务挂起,然后切换到其他的去运行。

  GigaHttpd 的 CPU 只负责响应网卡的数据,然后做两件事情,一个是响应静态数据请求,另一个是响应动态数据计算请求,以下称作计算任务。

  GigaHttpd 的 CPU 负载在单个 PC 内部,以及整个集群内部进行均衡。均衡的主要思想是动态改变数据的分布,即静态数据,或者动态计算任务在多个 PC 上的分布,根据需要动态调整,以此来支持单个数据的大量并发访问(例如热点视频实时全球直播)。均衡算法留待详细设计时考虑。

  GH 的 CPU 所管理的内存相互间隔离,只通过共同的内存页面交换数据。

  GH 模块加载后,为每个 CPU 准备好页表页目录,以及 main() 函数和堆栈,引导每个 CPU 进入 main() ,并进入内部的循环。同时修改 Linux 内核的 CPU 数据,让 Linux 看起来只剩一个 CPU。

  因为不考虑多任务(多进程、多线程),每个计算任务结束后,如果产生静态数据(例如论坛的帖子),则静态数据转成长期的数据保存在静态数据区。其他运行过程中需要的动态内存,都应该可以被当作空闲内存而重复使用。所以,GigaHttpd 不提供 C 语言中基本的 malloc() 函数,而是 main() 函数中提供一块静态的巨大的内存来做数据缓冲区。根据自身的实际需要,计算任务的开发者自己考虑这块内存的详细使用布局。另外,从性能和编程方便的考虑,如果堆栈可以接受,则可以预留巨大的堆栈内存空间供计算任务来使用。

  关于动态计算任务中的漏洞(例如缓冲区溢出)导致运行未知代码,或者进而被黑客利用的问题,可以给每个 CPU 设置不同的内存空间,防止一个 CPU 出现问题影响到其他 CPU 的地址空间。关于漏洞被黑客利用,植入黑客代码,修改其他 CPU 的页表页目录,进而修改整个系统数据的问题,留待以后的版本考虑。如果这个问题上升到非常严重的程度,则可以考虑回到 Linux 的进程模式,这个问题是要在安全与性能之间作出抉择,尽管进程模式也可能被黑客利用。

  关于计算任务“僵尸”的问题,也就是动态计算任务代码存在 bug,导致 CPU 进入死循环。这个可以利用定时器来检查判断,或者另外设置一个“监控”CPU,或者 CPU 间定期互相监控,当发现一个 CPU 进入“僵尸”后,就对其初始化,之前把僵尸现场,包括计算任务记录下来,同时对此计算任务设置标志,阻止其再次进入运行状态。被阻止运行的计算任务应给用户浏览器返回错误提示,同时也给系统管理员发出报警信号,以此来辅助快速定位问题发生点。

  总的来看,GH 的 CPU “永远不死”,一直循环处理计算任务(静态数据/动态计算任务)。


* 准备网卡驱动和协议处理

  GigaHttpd 中 CPU 的唯一任务就是响应网卡的请求,返回静态数据(例如页面),或者先进行动态的计算任务,一般生成静态数据,然后返回给用户。本版设计中,为了系统结构简单,PC 中的网卡与 CPU 数量一样多,一一对应。同时,像 CPU 有一个留给 Linux 一样,也留一个网卡给 Linux,处理类似 ssh 这样的连接。后续的版本中,可以考虑 CPU 数量与网卡数量不一致的设计。

  另外,为了实现 PC 间的负载均衡,需要把一个 CPU(或一组 CPU)计算出来的静态数据,复制到多个 PC 中,所以需要一块“复制”网卡。

  这样,GigaHttpd 的网络系统可以分为三个独立的子网。一个数据子网,连接所有的“数据”网卡;一个复制子网,连接所有的“复制网卡”;一个网管子网,连接所有的 Linux 网卡。本版中,为了系统结构简单,三个子网在物理上独立。

  在支持 10 亿并发请求的系统中,有 1 千台 PC,数据子网的网卡大约有 1 万块,在这个子网中绝对禁止广播包。复制子网有 1 千块网卡,这个子网内大部分是广播包,后续版本可以考虑划分 vlan,在 vlan 内广播。网管子网有 1 千块网卡,这个是普通的 Linux 网络。

  GH 模块加载后,从 Linux(或者 BIOS)里取得各网卡的信息,留下一个给 Linux,把其他的网卡从 Linux 里卸载(或关闭),然后把每个网卡分配给每个 CPU。在此我们需要一些事先准备好的设置参数,指定哪个网卡留给 Linux,哪个网卡作复制,剩下的网卡都是数据网卡。由此,在本版设计中,我们需要准备一个配置文件,包含 GigaHttpd 启动的各种参数。以后的版本中,可以增加自学习的功能,让系统在启动时自动探测每块网卡属于哪个子网。

  数据网卡和复制网卡分配给每个 CPU 后,CPU 在自己的 main() 中循环读取、设置网卡的状态和数据。本版设计中,为了结构简单,不考虑使用中断模式来访问网卡。总体上来说,可能中断模式对网卡的整体吞吐效率没有什么帮助,只是对编程有很大的好处(另外可能还有省电环保),可以支持多任务系统。GigaHttpd 的 CPU 只专注于响应网卡的数据处理,没有多任务的需求。

  对于花费较长时间的动态计算任务,会造成网卡和 CPU 的“堵塞”,导致其他的并发请求响应缓慢。关于这个问题,一方面,从总吞吐量来看,中断一个正在运算的请求,保留现场,然后去处理后续的请求,并不能增加总吞吐量,所以采用中断处理意义不太大。另一方面,对于计算量很大的计算任务,例如几千万人同时参与全球环境治理模型的发展演变,仿真展现三维效果,则需要一定规模的 PC 群来进行集中计算。可以把用户提交的数据,翻译后通过数据子网转发到集中计算服务器群,然后等待一个复制子网的数据更新,将更新结果返回给用户。对于另外的应用类型,例如商品竞价买卖,计算可以分担在很多 PC 上,每个 PC 只负责少数几个商品的竞价撮合,这样这些集中计算的 PC 来给用户返回计算结果。

  所以在 GigaHttpd 系统中,接收用户请求的网卡和给用户返回数据的网卡可能不是一块网卡,当然如果这种情况发生,则这两块网卡很可能不在同一个 PC 上。进一步,GigaHttpd 内部的静态数据会支持包含关系,例如一个网站的新闻页面,既包含 HTML 内容,也包含 Javascript 代码和 CSS 代码,同时 HTML 代码中还包含了一些随时变动的信息,例如广告或者计数器之类的东西。当然这些变动的东西可以用 AJAX 技术来动态获取,但这会增加与网站的交互量,在为了提高效率的考虑下,GigaHttpd 可以把这些内容一次性打包的发给用户端,但是这些东西因为经常变动,在 GigaHttpd 内部是动态生成的,各部分都保存成独立的静态数据。所以 GigaHttpd 支持静态的数据包,里面是链接,指向各静态数据的内部地址。于是,在给用户返回数据的时候,就把这些静态数据包依次给用户发回去。但是这些静态数据包分散在不同的 PC 上,所以接收用户请求的网卡(以及CPU),会把这个请求依次转发给其他的网卡,其他的网卡依次把数据给用户发回去。

  再考虑一个类似的情况,电影。把整部高清电影当作一个静态数据显然不可取,另外也不是所有人都会每次都把整部电影全部看完,还有要考虑允许在电影中插入广告,所以把电影分段保存,应该是个好主意。

  当然这最后会产生系统内部复杂度,与客户端效率之间的平衡,实际应用中可以根据情况来调整。

  整体来看,静态数据包应该是躲不过去的。

  在静态数据包的返回过程中,整个 GigaHttpd 所有的数据网卡返回的数据包中的 IP 地址是相同的,对用户端是透明的。

  本版设计中,对于数据网卡,GigaHttpd 的协议处理程序只处理 IP 包,以及数据网卡间的转发包。IP 包包括 TCP 和 UDP 包。支持 UDP 是为了保留将来对网络游戏之类应用的支持。

  好吧,惭愧一下,根据目前的实际网络情况,本版网络协议只支持 IPv4,IPv6 留待后续版本实现。

  TCP 协议方面,初步实现 RFC-793。任何一块数据网卡都可以接收 TCP 连接请求(SYN包),对应的 CPU 接收后做基本检查,如目的端口、校验等,GH 根据检查结果返回响应(ACK包)、或者拒绝(FIN包)。如果检查通过,GH 申请一页内存,来存放 TCP 连接信息,包括客户端的 IP、Port、Sequence Number,以及连接状态,以及 GH 端的 Sequence Number 等。

  TCP 的端口事先记录在配置文件中,GH 模块启动时读入内存。运行中的端口改变通过广播子网来设定。

  客户端接收到 GH 的 TCP响应(ACK)包后,向 GH 的这块网卡发来第一个数据包,包含 TCP ACK,包内的数据部分是一个 HTTP 请求包的一部分。后续逻辑如下:

    检查 TCP 包,如 Sequence Number 等;
    if (检查通过) {
      解析 HTTP 请求,读取 URL;
      计算客户请求的对象(静态数据或动态计算任务)所在的 PC;
      if (对象在本 PC) {
        将此 TCP 的状态信息(包括客户端发来的 HTTP 请求),挂在对象的客户请求表中;
        调用响应的函数进行进一步处理,具体见后;
      } else {
        将此 TCP 的状态信息(包括客户端发来的 HTTP 请求),通过数据子网发给静态数据所在的 PC 的某块数据网卡;
        等到对方的接收确认后(异步等待),释放本地的内存页;
      }
    } else {
      向客户端发送拒绝(FIN包),并释放内存页;
    }

  如上,因为第一个 TCP 请求数据包要在 GH 数据子网内转发,为了高效率最好用一个以太数据包完成,所以 GH 给客户端的第一个 ACK 中的 Window 大小不超过 1K。

  对于要进行用户身份确认的应用,客户登录页面的信息要放在 URL 中发给 GH,而不是通过 HTTP POST 的 BODY 来发送。GH 接收后,会首先分析用户名等信息,如进行 hash 计算,然后将这第一个 TCP 请求发给用户信息所在的 PC 的某块网卡。在 10 亿用户的应用中,10 亿用户信息会存放在 400 台 PC 上,总共占用 4TB 的内存,在数据子网上有大约 4000 块网卡响应用户身份验证。用户身份数据与对象数据的分布优化留待后续版本实现。

  本版设计中,来自外网的 TCP/IP 包如何发送到准确的数据子网的目的网卡,由前端的负载均衡设备来实现。当然这要与负载均衡的设备开发商进行合作。GH 在明确了一个 TCP 的真正目的网卡后,通知负载均衡设备,此后负载均衡会根据客户端的 IP/Port 信息,以及 GH 的 IP/Port 信息,自动把后续的 IP 包都转发到特定的 GH 数据网卡。当然在一个 TCP 的连接完成之前,GH 端的目的网卡会发生改变。例如 GH 要把一个大尺寸的图片数据发送给客户端,开始的用户身份确认阶段是某一个数据网卡,而身份确认后,大量返回图像数据的阶段,是另一个数据网卡。

  本版设计中,TCP-SYN flood 攻击由防火墙设备来处理。

  本版设计中,以太网协议与 IP 协议只实现到可以支持 TCP 即可。

  UDP 协议支持留待后续版本实现。

  ICMP 只做最基本的实现,能够用于简单地测试网络联通。本版设计中,客户端并不知道是具体哪块数据网卡在回应 ICMP。

  本版设计中,SSL/TSL 由前端负载均衡来实现。


* 加载静态数据

  本节主要描述 GH 内部的数据分布与访问方式。主要挑战在于静态数据的快速定位,以及数据量的持续增长。

  静态数据的快速定位,就是如何快速找到用户 URL 所指定的数据,例如某个图像文件。GH 的静态数据全部存放在硬盘上,部分存放在内存中。GH 的静态数据直接通过内存地址来定位,也就是 URL 中直接包含内存地址,类似于 http://x.y.z./1111。

  静态数据既包括内容/页面数据,也包括用户数据。所以用户登录后,后续的 URL,无论是浏览器直接点击,还是 AJAX 访问,都类似于 http://x.y.z./1111/u222/sSSSSSSS,其中 1111 是页面数据的内存地址,222是用户数据的内存地址,SSSSSSS 是用户登录后的 Session。

  一个 Web Server 中的页面数据是从零开始增长的,注册用户也是从零开始增长的。

  静态数据的产生。无论是用户、网编,还是网管,向 GH 提交一个数据后(HTTP 协议),一个 GH 的动态计算任务将被启动。计算任务的计算完成后申请一个内存地址,然后将计算结果复制到那个地址。

  GH 的静态数据在内存中以 4M 为一个基本单位(下称数据块)来分布。也就是每申请一个内存地址都会得到一个边界为 4M 的内存地址。数据块都有一个标准的头部。保存一些基本信息,如数据长度,有效状态,数据的产生时间,最后一次访问时间,访问次数,校验和,授权码或者授权块的地址(用来设定数据块的访问保护),下一个数据块的地址,硬盘上的备份位置,TCP 的连接表指针(此数据正在向客户发送的所有连接状态信息),源 PC 信息(此数据是别的 PC 生成后广播复制过来的),等等。头部之后是实际的数据。

  如果单个数据的尺寸大过 4M,则可以保存成多个数据块。

  GH 内部每个 CPU 都有一个内存块链表,记录所有尚空闲的数据快区域。

  GH 的动态计算任务调用内部函数来申请数据块,函数参数中包含数据块的尺寸。该函数从内存块链表中取得一块或多块,然后根据实际需要的尺寸来映射物理内存页(物理内存页尺寸初步定为 4K)。也就是我们不会让博客或论坛中只有一个字的“顶”帖占用 4M 的物理内存。

  GH 中的数据可以被删除,删除后剩下的地址空间以后可以继续利用,物理页被回收。

  除非实在迫不得已,否则我们不会考虑支持任何 CPU 互锁的情况存在,因为我们支持超过 1 万个 CPU,超过 1 千台 PC。所以在申请内存地址方面,我们为每个 CPU 预先留下足够的内存地址空间。多亏我们是 64 位系统,呵呵。

  我们支持应用系统从一个 CPU 开始起步,这应该是最符合实际情况的做法。以后随着应用系统访问量、数据量的增加,再逐步增加 CPU/网卡,增加 PC;而且我们支持动态增加(热插拔);另外,在系统检修、排除故障时,我们要支持动态缩减 PC(如需要保证客户服务不中断,则需要在明确的内部数据复制指令完成后)。所以,我们一开始就为应用系统的最大可能数量的 CPU 们留下足够的内存地址空间。每个 CPU 的数据地址空间暂定为 4T(2的42次方),1 万(约 2^14)个 CPU大概会占用 64KT(2的56次方)的地址空间。

  一个数据块占用 4M 的内存地址空间,4T 可以容下 2^20(约一百万)个数据块。

  所以数据块的地址,由两部分组成,CPU 序号与 CPU 地址空间内的一个地址(怎么这么别扭,以下简称 CPU 内地址)。我们把一个 CPU 的序号用 cpu_n 来表示。

  数据块内存地址的长度为 56 位,以 4M 为边界,减去 22 位,还剩 34 位,不到 4.5 个字节,翻译成 16 进制字符串需要 9 个字节,格式如下:

    ........  54321098765432  10987654321098765432  1098765432109876543210
    [ 8 bit]  [                          56 bit                          ]
                [   14 bit   ]  [       20 bit     ]  [      22 bit        ]
                [  CPU 序号   ]  [     CPU 内地址     ]  [    边界,省略        ]
                [          数据地址,34 bit          ]

  数据块地址在 URL 中可以采用省略写法,用冒号分隔 CPU 序号和 CPU 内的地址,格式为: cpu_n:xxxx。

  本版设计中,我们给每个 CPU 分配 1GB 的物理内存。随着数据量的增加,一个 CPU 的地址空间内数据总量接近 1GB 的时候,CPU 将最老的、访问次数最少的数据从内存中丢弃(之前已经备份到了硬盘上),回收物理内存,进行重新分配(映射)。

  所以,一个应用系统规模逐步增加过程中,GH 内部的数据分布变化大致是这样的:

  (1) 某个公益/商业创意团队发布自己的一个应用网站,只有一台 PC,双网卡,2G 内存,一个双核 CPU,GH 用其中的一个 CPU Core,这个 CPU 接收到的用户数据都保存在 4T-8T(4T 以下保留给系统)的地址空间中,并且数据被备份在硬盘上。

  (2) 随着访问用户的不断增加,提交的数据越来越多,达到几百 M 或者几个 G 的时候,团队新增一个 PC,四块网卡,4G 内存,一个四核的 CPU。启动“上线”命令后,GH 自动把在第一台 PC 上的大部分经常被用户访问的数据都复制到第二台 PC 的内存中,此后两台 PC 共同给用户提供服务。所以第二台 PC 内部的内存中,在 4T-8T 的地址空间中,保存着第一台 PC 的“热点数据”,这样用户访问以前的数据(指定了内存地址)就可以均衡到两台 PC 的 4 块网卡/4个 CPU 上。第二台 PC 上的 3 个 GH CPU 新接收到的用户数据,就会存放在 8T-12T/12T-16T/16T-20T 的地址空间中,并且备份到本地硬盘中。当然第一台 PC 新接收的数据,还会放在自己的地址空间 4T-8T 中。

  (3) 此后两台 PC 的热点数据会继续互相复制。另外,在 CPU 的空闲期,两台 PC 将对方的全部数据都备份到本地硬盘中。以此来保证一台 PC 突然故障后,另一个 PC 可以实时接管所有的用户请求。具体硬盘的数据备份如何恢复到内存中来见后。假设是第一台 PC 发生了故障,那么第二台 PC 中的 GH CPU 接受的用户数据继续保存在自己的内存地址空间中,即不再写如到 4T-8T 中。

  (4) 假设第一台 PC 故障后无法修复,团队又增加了一个新 PC,4G 内存,四块网卡,一个四核的 CPU。启动上线命令后,GH 自动把 4T-8T 的数据全部复制到第三台 PC,这样第三台 PC 的内存地址分布为 4T-8T/20T-24T/24T-28T。第二台 PC 的热点数据也会复制到第三台 PC 上,实现负载均衡。

  (5) 以此类推,可以不断地增加 PC,然后不断地摊匀热点数据,比如首页、爆炸性实时新闻、热点视频直播等,可以在每个 PC 的内存中都进行复制。以此来应付访问量的持续增长、热点数据的不断变换。

  (6) 数据量积累方面,当一个应用系统的规模从一台 PC 增长到一千台 PC 的时候,最早的第一台 PC 上的数据可能积累得最多,有可能会占满 4T-8T 的内存地址空间(因为哪怕再小的数据都占用 4M 地址空间)。关于这个问题,后续版本会考虑内存地址复用来解决。原因很明显,很多数据在网站保存后,后续的访问量非常小,比如一年前的新闻报道、两年前的时尚杂志内容、三年前的电子商务交易记录明细、某个糊涂鬼忘记密码后留下的信箱邮件以及博客文章,等等。为此类很长时间不访问的数据保留唯一的内存地址空间,显然不太合适,我们只要支持在很多年后,如果有人来访问,能让这个数据再次从硬盘上被找到,重新读取回内存即可。

  (7) 每台 PC 的数据都在相邻的 PC 里做硬盘备份,以减少系统的运行风险。

  本版设计中,还需要考虑一个实际情况,针对目前主流的 4 核 CPU 系统,中小型应用中,单独拿出来一块网卡来做复制子网可能不划算,那就先在数据子网中做数据复制,尽量少用广播包。

  本节最后,简单设想一个应用,一个不超过 60 个 CPU 的中小型应用,例如一个很多人参与的 web 游戏,每个 CPU 有 1G 物理内存,假设每个数据(虚拟城市)的实际平均尺寸为 1M,则每个 CPU 理论上可以支持约一千个城市。但是用户数据也是要占用内存的,假设每个用户的数据占用 1K,每个 CPU 如果支持 10 万个用户同时在线,则同时在线用户数据会占用 100M 的物理内存。剩下 900M,去掉系统可能占用的基本内存 100M,每个CPU 应该还可以支持 800 个虚拟城市。假设热点城市占总城市数的1/3(热点城市数据会在多个 PC 内存中复制),则平均每个CPU 应该可以支持超过 500 个虚拟城市。所以整个系统应该可以支持 600 万用户同时在线建设一个有 3 万个城市的虚拟星球,呵呵,在此基础上可以进一步展开各种气候、生态、商业等等创意。如果同时在线用户与总注册用户的数量比例是 1:3 到 1:5,那么这个系统应该可以支持 2000 万到 3000 万注册游戏用户。

  60 个 CPU,需要 20 台 PC,每台 PC 配备一个 4 核 CPU、4G 内存、几百 G 的硬盘。按照 2008 年的电子市场行情,PC 总价应该不会超过 20 万元。呵呵,性价比有些不真实,还不知道哪里计算失误了,不过没关系,幻想/梦想就是动力,我们继续前进。


* 硬盘数据备份

  本节主要描述 GH 内存中的数据如何备份到硬盘上,以及如何读回内存。

  动态计算任务产生静态数据后,存放在了内存中,之后应该及时在硬盘上作备份。内存中的静态数据是以(连续)数据块的形式存储的,有一个唯一的内存地址,还有很多附加信息,如相关日期、访问次数、授权信息等等。这些数据保存在硬盘上也继续采用这种格式。为什么采用这种格式,而不采用像 ext2/3 之类的传统文件系统格式?举个例子,对于视频网站这样的应用,一旦有一个用户开始从头播放一个数据尺寸几百 M 甚至几个 G 的电影,我们如果立刻把整部电影都读入内存中,显然不合适。另外,用户可能从影片的任意部分开始播放电影,所以我们应该支持读入任意一个数据块。

  本版设计中,每个 CPU 在硬盘上都拥有一块自己的“独立空间”,避免访问时产生互锁问题。本版设计中,为了设计简单,每个 CPU 的“独立空间”就是硬盘上的一个逻辑分区。每个 CPU 在内存中拥有 1GB 的物理内存,按照 1:50 的比例,每个 CPU 的逻辑分区大小 50GB,实际应用中可以根据硬盘容量及网站数据量进行调整(在网站系统的规划阶段)。这样,一个 4 核 CPU 的 PC 中,硬盘上有 150GB 空间应该保留给 GH。另外,GH 会把每一个静态数据在相邻的 PC 上备份,这意味着每个 PC 上还可能要保存邻居 PC 上的 150GB 的数据。所以,在这个 PC 初始化时,应该在硬盘上保留超过 300G 的空闲空间。

  现阶段的一个中小型应用,60 个 CPU,每个 CPU 保存 50GB 的数据,总共可以保存 3TB 的数据。如果应用中需要保存大量数据,例如邮件系统,则可以增加每个 PC 上硬盘的容量与个数。

  GH 中,每个上线后的 PC 都有一个自己的序号,并且与系统中的其他所有 PC 自动商量,与一台或若干台 PC 建立数据备份约定(根据每台 PC 硬盘中的空闲空间而定)。

  数据块在硬盘上的存储,以簇(10KB,20 个扇区)为基本单位。内存中的数据块,头部记录其在硬盘上的起始簇的位置,如此有助于数据块内容被修改后及时同步。

  在 CPU 逻辑分区的开始部分(头部),预留一块空间用来记录该分区内所有数据块信息以及簇信息,簇信息里有指向硬盘扇区的簇指针。一个大数据块存放在多个簇中,所以簇信息里也包含下一个簇的指针。数据块信息的长度初步定为 128 字节,簇信息的长度也初步定为 128 字节,可以包含数据块的更多附加信息(预留)。

  本版设计中,为了简单,CPU 逻辑分区中的空间一直“向后使用”,到达尾部后再回到头部继续。即,每次要向 CPU 逻辑分区写入一个数据块时,总是写在分区内的数据尾端,而不考虑利用分区中数据块被删除后留下的空白簇。这些空白簇会在下次循环回来后再次被使用。这种方式也有利于支持 U 盘。

  本版设计中,同样为了简单,数据块信息空间也一直“向后使用”,但不循环。本版设计不考虑数据块地址的复用,所以数据块地址是一直是持续增长,因此,在硬盘上的数据块空间内的数据块地址都是从小到大排列的,当然一个数据块被删除后会留下空洞。当数据块空间使用达到最后时,CPU 专门整理一次这个空间,从前向后去掉空洞,以便容纳新的数据块。

  CPU 逻辑分区也有一个头部,存放剩余簇数、簇顶端、剩余数据块空间、数据块空间的顶端等信息。

  在给定一个数据块内存地址后,如何从硬盘上快速找到这个数据块,则利用数据块空间的顶端这个数据,进行折半查找。

  CPU 逻辑分区内的各种优化,以及与 DMA 相关的各种优化,留待后续版本。

  GH 内部的数据之间的联系,主要通过数据本身来记录。解释一下,比如 GH 内部大量存储网页,每一个网页都只能通过其他的网页中的超级链接(内存地址)来找到;再比如一个商品交易网站,每个商品通过一个商品代码(内存地址)来找到。

  类比一下,目前记录数据之间的关系主流方式有文件目录、数据库、全文检索这三种方式。文件目录支持遍历所有目录,找到所有数据。数据库支持 SQL 语句,可以找到所有数据,当然还可以作统计。全文检索支持按关键字搜索数据,但不一定能找到所有数据。

  本版设计中,暂不支持以传统文件目录方式来访问一个硬盘分区内的所有数据,但可以提供简单浏览功能,如在“一个目录下”列出来硬盘上所有的数据,还可以包括数据类型,比如是图像、博客文章、商品、交易记录、游戏装备、NPC 怪物等等。

  本版设计中,支持将硬盘上一个 CPU 逻辑分区内的数据,复制到 U 盘中作为备份。备份出来的数据,需要的情况下,可以从硬盘中删除,例如超过若干年的商品交易记录、财务数据等。

  本版设计中,不支持对所有数据采用 SQL 语句的访问方式,也就是不支持任意的数据排序、统计、约束关系等。原因很简单,对存在于一千台 PC 的硬盘中的数据进行任何排序或统计,都可能花费太长的时间,而且在排序、统计中一旦锁定所有数据,则可能会严重影响性能。

  本版设计中,如果需要对整个系统中的数据进行深入分析,包括任意排序及统计,可以基于备份出来的 U 盘上的数据来进行。

  本版设计中,数据间的约束关系(类似数据库内数据表间的约束关系),由动态计算任务自行维护。例如网管删除一个博客用户,动态计算任务负责删除其名下的所有博客文章。

  本版设计中,对于应用中所必须的排序与统计,在动态计算任务内“逐步积累”,也就是事先设定好排序与统计的算法与存储空间(数据块),没增加一个数据,动态计算任务就更新排序索引,以及统计结果。例如,对于一个在线商品竞价交易所,每撮合成一笔交易,就累加进当日个人与交易所的总成交额;如果应用中需要统计当日交易额的前一百名用户,则交易成功后判断并更新每个 CPU 下的前一百名记录。假设整个系统有 500 台 PC,1500 个 CPU,每个 CPU 管理 100 种商品,总共管理 15 万种商品;每个 CPU 管理 10 万在线用户,总共管理 1.5 亿在线用户( 5 亿注册用户)。这种情况下,计算任务可以定期每 10 个 CPU 将自己的前一百名记录发给某一个 CPU 进行排序,得出这 10 个 CPU 范围内的前一百名。以此类推,经过四轮,就可以定期的得到整个交易所的交易额前一百名用户,商品交易额统计、商品逐级分类统计等也可以如此。

  全文检索留待后续版本实现。


* 普通用户

  GH 中的用户分为系统用户(系统管理员帐号),与普通用户(网站的注册用户)。本节中“用户”均指普通用户。

  系统用户帐号信息与普通用户帐号信息在 GH 中都以静态数据块的形式存在,都有一个静态内存地址。

  应用程序中的用户,无论分多少级,GH 都按普通用户对待,应用级别用权限来标志。例如一个有很多层级的论坛,每一级用户权限由权限码来设定。

  如果应用中需要给用户分“组”,则“组”信息也是静态数据块。用户与组之间的数据关系(一对多/多对多)由应用程序(动态计算任务)自己维护。例如每个用户可以有很多“好友”,所以每个用户可以有一个“好友组”;每个用户可以属于很多圈子,圈子在内部也可以设计成组。

  本版设计中,仅支持用户通过用户名和密码来登录。为了安全,用户名和密码不放在可见的 URL 中提交。

  如何避免注册用户重名是个麻烦问题,因为我们可能有 1 万个 CPU 同时接受用户注册,不可能在任一时刻锁定所有 CPU 的内存;而且我们支持超过 10 亿用户,即使每个 CPU 维护一个用户名索引,内部查找的负担也很大。所以,本版设计中,一个用户注册成功后,GH 自动给用户名后面增加一个字符串表示的内存地址,格式如“-cpuid_CPU内地址”,cpuid 和 CPU 内地址合计最长 9 个字节。所以一个用户名的将变成类似“张王李-1234_12345”,优化一下,我们用一个 CPU 的名字来代替 cpuid,所以结果就类似“张王李-黄山12345”,“黄山”是一个 CPU 的名字。反正我们要在每个 PC 内部保存一个所有 CPU 的列表,因此给每个 CPU 增加一个名字(及索引)不会增加多少负担。此种方式下用户注册时肯定不会重名了,呵呵。

  但是,显而易见,无论如何优化,这种命名方式还是很难看的。其实问题的本质是解决名字字符串与一个内存地址的对应问题,分布在很多个 CPU 上。我们可以设计一个标准的接口,允许应用程序开发人员可以自行设计替代函数,实现更好的对应关系,和更快速的增加/查找方式。

  用户密码保存成原文,还是一个校验和,支持由应用开发团队来决定。

  用户产生(提交)的数据,与用户帐号数据都尽量保存在同一个 CPU 的数据空间内,减少 CPU 间的跳转。例如博客的作者与文章、网店的老板与商品等。

  用户登录验证通过后,随机生成一个 Session,长度 32 位(8 字节长的字符串),记录在用户的帐户信息内。本版设计中,用户的帐户信息内可以放置 10 个 Session,也就是允许一个用户帐号最多同时从 10 个客户端登录,例如用户可以同时从电脑、汽车、手机、卫星定位器、人体植入装置等设备中同时登录自己的帐户,开展应用。

  为了快速解析,Session 放在 URL 中来传输(避免使用 cookie)。

  10 个 Session 位置循环使用。每个 Session 可以设定任意长的过期时间。

  GH 支持在任意客户端界面上使用图形验证码。GH 的每个 CPU 都在自己的内存空间保存 100 个验证图形,需要使用验证码时随机从其中选出一个来使用。每个验证图形都有生命期,一分钟。

  对于探测用户名、密码、图形验证码的行为,GH 会锁定帐户及客户端 IP 地址一段时间。


* 普通用户的权限

  用户的权限,也就是用户与静态数据之间的关系。

  静态数据在其头部记录拥有者的ID,也就是用户的内存地址。

  静态数据在其起头部记录读写权限标志,64 位,每两位代表一个标志,共 32 个标志。两位标志中,前一位代表是否必须遵守,后一位是标志。本版设计中,支持 5 种标志:不保护、拥有者读写、读取授权函数、更改授权函数、删除授权函数,其余的标志位留给应用程序去发挥。

  “不保护”的数据任何人都可以读取,但只有拥有者可以更改/删除。例如网站的新闻,谁都可以看,但只有上传者可以更改。

  “拥有者读写”的数据,顾名思义,只有拥有者自己可以读取、更改、删除。例如博客中的个人私密文章,资金帐号中的信息等。

  设置了授权函数标志的数据,其头部还要记录对应函数的内存地址。例如一个数据设置了读取授权函数,当某个用户读取时,GH 发现其不是数据的拥有者,于是调用此函数,函数入口参数包括拥有者ID、读取者ID、数据ID,函数的返回值指明是否可以读取。这个函数可以由应用程序开发团队设计,这种模式可以支持复杂的数据间授权关系,例如好友,圈子,等等。

  举个例子,网站中的一则新闻,新闻内容由某个编辑提交,在新闻正式发布到网页之前,这个编辑就有所有读写删的权限。这篇新闻被上一级编辑审核后,当初发布的编辑就没有了删除的权限。在文章被终审通过发布后,当初发布的编辑也没有了删除的权限,删除权限给了另外的用户。对于这个应用,这则新闻数据的授权标志,刚提交时:拥有者读写;经过第一轮审核,发布前变成:读取授权函数、更改授权函数、删除授权函数;发布后增加一个:不保护。具体的编辑之间的授权关系,由新闻网站的开发团队来设计。

  请注意,GH 中的数据是直接对外的,即只要给定一个数据的内存地址(当然不是内部数据),就直接可以返回数据。不像传统应用,会有数据库/数据文件等后端。所以数据的权限保护是需要重视的。


* 系统用户与权限

  系统用户的信息保存在每一个 CPU 的内存空间内。本版设计中,GH 内系统用户的数量是固定的,最多 32 个,第一个系统用户的用户名是 root,这个名字永远不能改变。

  本版设计中,只有 root 能创建其他系统用户,修改其他系统用户信息,如用户名/密码、权限等。

  设计系统用户的目的是维护系统的运行。所以系统用户可以:
    检查、校验所有的静态数据;
    查看/手工调整热点数据的分布;
    查看每个 CPU 的运行情况;
    设置每块网卡的参数,查看每个 CPU 上的网络连接状况;
    设置/查看硬盘数据的分布与完整性;
    上传/加载/卸载计算任务;
    处理 PC 的上线与下线;
    查看整个系统的负载,调整运行参数;
    调试系统、调试应用程序,发现并解决系统中的 bug;
    备份数据;

  所有系统用户的修改行为都要被记录到审计日志。

  每个系统用户都可以被赋予一定的权限。

  系统用户的数据在硬盘上的单独分区保存。

  系统用户的密码都以校验和的形式保存。

  对于每个系统用户的任何修改,都会被同步复制到其他所有的 PC 上,并保存在所有的硬盘中。

  系统用户不能创建普通用户。普通用户里的第一个管理员用户(如果需要的话),由应用程序自己创建,自行维护。


* GH 的 Shell(命令行界面)

  继续保留命令行界面的好处很多,比如支持命令的批处理。命令行界面仅供系统用户使用。

  系统用户在客户端浏览器上操作命令行界面。命令行界面是个用 Javascript 开发的应用程序,把系统操作员输入的命令直接发给 GH,然后将运行结果取回来,显示在浏览器上。

  Shell 命令样例:

   [中文] 列出_用户_cpu 3 范围 1-100
   [eng] ls_user_cpu 3 range 1-100

  以上两个命令可以列出来 id 为 3 的 CPU 上所有用户中的第 1 到 100 个。

  本版设计中,每一个命令都支持中/英两种语言。

  本版设计中,Shell 暂不支持批处理。

  后续版本设计中,Shell 命令既可以运行在客户端,也可以运行在 GH 内,作为动态计算任务来运行,例如系统启动时的 Shell 命令、一台 PC 上线时的命令等等。


* 动态计算任务

  至此,GH 的数据结构、运行模式基本清楚了,该考虑程序如何运行了。

  GH 内部的程序分成三部分:GH 内核,动态计算任务,函数库(类似与 Linux 的动态链接库)。

  GH 内核程序实现基本的功能:内存管理,网卡驱动与协议处理,硬盘分区管理,静态数据响应,动态计算任务加载,等等。本版设计中,不支持 GH 内核程序动态升级。

  函数库是事先准备好的程序,可以被动态计算任务调用。本版设计中,不支持函数库的动态加载/卸载,留待后续版本实现。

  动态计算任务是 GH 内部的应用程序。本版设计中,不支持动态计算任务的动态加载/卸载,但是在系统结构设计上,要预留支持;另外,也要预留支持网站应用的多次开发,即允许用户来控制其他用户的应用程序的加载/卸载/统计/安全审计等,例如目前流行的 Web API。

  GH 的内核运行在 CPU 的内核级(Intel-Ring0),GH 的每一个动态计算任务可以被单独指定运行在内核级,还是应用级(Intel-Ring3)。每个计算任务在编写/编译时就要支持在内核级和应用级的平滑移动,即不用修改二进制代码就可以切换运行级别。

  如果动态计算任务运行在应用级,情形就会类似传统的操作系统,一个应用程序被加载成一个进程之前,内核需要给他构造内存空间,映射函数库,准备好一系列系统调用来供其访问硬件,然后开始运行。幸运的是我们只需要制造一个“简化版的进程运行环境”。应用级计算任务处理完数据后不会像传统的进程那样退出,而是继续留在内存中,回到它刚被加载后的初始状态,其所需的运行环境也一直存在,等待下一次被调用,继续处理数据。

  从网卡接收进来的数据,通过内存页面映射直接提交给应用级计算任务,也就是没有内核与“进程”内存间的数据复制过程。而计算结果,也直接保存成静态数据,通过内存页面映射返回给 GH 内核。

  内核级的应用程序就相对简单,初始化时映射好所有的库函数即可。

  从安全角度考虑,理论上,应用级计算任务如果发生错误或被黑客利用,只能产生有限的影响或破坏;而内核级计算任务被利用,则可以产生灾难性后果。但是内核级与应用级的性能差别也可能产生重大影响。所以我们把这个抉择留给应用开发团队,如果应用开发团队认为自己的计算任务足够安全(经过充分的测试与很长时间的实际运行),可以设置计算任务运行在内核级,提高性能;否则,可以先运行在应用级,增加安全性。

  类似静态数据,每个动态计算任务也都有一个唯一的内存地址,用来快速定位。所以客户端提交数据时,URL 会类似于:
    http://x.y.z./P5555/u222/sSSSSSSS
  其中的 P5555 就是指内存地址为 5555 的一个动态计算任务。随着这个 URL 提交的数据可以在 URL 中,也可以在 POST BODY 中。

  动态计算任务的 ID(内存地址)同样由 CPU 编号和内存地址组成。本版设计中,一个动态计算任务的程序所占的空间,包括代码、静态变量等,不超过一个数据块的尺寸大小,4MB。后续版本设计支持更大的程序。

  为了支持复杂的分布计算,GH 中的每个动态计算任务可以由多个“计算阶段”组成。一个计算任务从第一个计算阶段开始运行,处理数据,处理完成后交给下一个计算阶段(可能转到其他 CPU 上)继续处理,直到全部完成。

  细化一下,举个例子,我们的一个网站要支持 10 亿注册用户同时在“一张纸”上画图。画图功能在网站内部是由一个计算任务实现的。假设在某个时刻,10 亿用户同时(一秒钟内)在“纸上”画了一笔,所有客户端的程序在一秒中内将 10 亿个 HTTP 请求发向 1 千个 PC 的 1 万个 CPU。如果这个绘图的计算任务只在一个 CPU 上运算,则后续过程应该是:所有的其他 CPU 都将绘图数据发给这个 CPU 来运算,绘制出来最终结果图像,复制到其他 CPU 上返回给用户,显然,采用这种办法,那个负责运算的 CPU 累死也算不完(在用户等待画下一笔的耐心范围内)。

  所以,本版设计中,我们把这个计算过程分配到很多 CPU 上,共同完成。继续用画图这个例子来说明。一个网站上可以有很多计算任务,每个计算任务在每个 CPU 的内存中都有备份。也就是画图这个计算任务一加载,则立刻被复制到所有 CPU 的内存中。另外每个计算任务都有一个“协调人”,负责将各计算阶段分配到其他 CPU 中,当然也包括自己。

  所以,画图这个计算任务的运行过程大概是这样的:

  (1) “图纸”上最初是一片空白。CPU_1(假定)接收到第一个 HTTP 请求,里面是用户的一个绘制动作。这个 HTTP 请求的 URL 中包含绘图计算任务的内存地址。GH 内核在内存中找到这个计算任务,计算任务的头部信息记录着应当由 CPU_7(假定)来协调,于是 CPU_1 将绘制请求发给 CPU_7,等待回应(异步等待)。

  (2) 再看 CPU_7,如果 CPU_7 在一定时间(一秒钟)内,从其他 CPU 处只接收到少量的计算请求(实际画画的人很少),总运算量在自己可以接受的范围内,则启动运算,然后将运算结果返回给各 CPU,或者直接通过自己的网卡返回给客户端(接管 TCP 连接)。

  (3) 如果 CPU_7 同时在极短的时间内接收到大量计算请求,远远超过了自己的计算能力(或者自己本来就在进行着另一个负载很重的计算任务),则 CPU_7 会广播一个通知,告诉所有 CPU 先自行进行绘图计算任务的第一阶段的计算,并同时开始请求相对空闲的 CPU 承担第二阶段的计算。以此类推。

  (4) 所以,10 亿个绘图请求,先在 1 万个 CPU 上进行第一阶段(第一秒)的计算,每个 CPU 处理 10 万个绘图请求,共绘制成 1 万个第一阶段的图像,然后每 100 个 CPU 将图像发送给一个 CPU 做第二阶段的运算(假如每个 PC 有 10 个 PC,则可以增加一个中间阶段,先在本 PC 内运算)。共有 100 个 CPU 接收数据后进行第二阶段的运算(第二秒),运算结束后将结果发送到一个 CPU 做最终的合成运算(第三秒),然后将计算结果广播给所有 CPU,返回给用户(第四、五秒)。

  继续幻想一下,考虑一个更复杂的应用,一个虚拟的股票交易所。虚拟交易所支持 10 亿用户同时在线交易,支持 10 万支股票。10 万支股票分散在 1 万个 CPU 上,每个 CPU 管理 10 支股票。在这个应用中,整个市场中的热点股票是随机出现的,理论上,任何一支股票的交易规模都可能瞬间从零增长到 10 亿级,在这种情况下,全系统范围内动态调整撮合运算的负载均衡就很有必要了。所以,在后续版本中,我们可以支持按照数据和计算阶段共同来调整计算过程的全局分布,即支持每支股票都是一个运算协调人。

  动态计算任务的二进制程序格式,留待后续版本考虑。

  计算任务的同步(互锁)问题,留待后续版本考虑。


* 系统的启动与上线

  留待后续版本考虑。


* 客户端的结构与设计

  浏览器,主要考虑 AJAX 模式。留待后续版本考虑。

  手机应用。留待后续版本考虑。


* 应用程序的开发与调试

  程序上传。留待后续版本考虑。

  程序编译。留待后续版本考虑。

  printf 与日志。留待后续版本考虑。

  在多个 CPU 上单步调试,类似远程 gdb。留待后续版本考虑。


* 项目实施

  留待后续版本考虑。


* 系统迁移与共存

  在别的平台上已实施的系统如何迁移到 GH 上来,数据迁移、程序重新开发。留待后续版本考虑。

  请注意我们的系统和 Linux 可以共存在同一台 PC 上(起码现阶段如此),所以我们可以支持一个网站由 GH 与 Linux(及其上的应用系统)共同组成。留待后续版本考虑。

GigaHttpd 设计思想 0.1 版,请大家帮忙看看,是否可行,谢谢!

04-08

项目网址: http://gigahttpd.sourceforge.net/rnrnrnrn版本:  0.1rn提交时间:2008-04-08rn本版作者:鲁义明 (Yiming Lu) lu.yiming.lu@gmail.comrn所属:  GigaHttpd 开发文档rnrnrn* 先说实话rnrn 写这一版设计思想的时候,我知道自己还很无知,很多想法可能都是错的,甚至整个设计都是彻底不可行。不过没关系,让我们来持续改进。rnrnrn* HTTP Server 功能的简单描述rnrn 接收一个 HTTP 请求,返回请求的数据。rnrnrn* 功能细化一下rnrn (1) 建立一个 TCP 连接。rnrn (2) 接收 HTTP 请求,GET 或 POST,验证身份 Session,解析 URL。rnrn (3) 从所有 Object 中找到对应 URL 的 Object。rnrn (4) 如果这个 Object 需要计算(比如 Post Body 中解析出来数据进行运算 ),则启动计算过程,保存计算结果。rnrn (5) 将这个 Object 的数据返回给用户。rnrnrn* 我们面对的挑战rnrn 每秒处理并响应 10 亿个 HTTP 请求,每个请求处理过程都要经过以上的步骤。rnrnrn* 我们可用的资源rnrn (1) 1000 台 PC。rnrn (2) 每台 PC 上有 8 到 16 个 64 位的 CPU Core。rnrn (3) 每台 PC 上有 8 到 16 GB 内存。rnrn (4) 每台 PC 上有 8 到 16 块 1G/10G 的以太网卡。rnrn (5) 每台 PC 上有两块 1TB 的硬盘,通过硬件 RAID 卡模拟成一块。rnrn (6) 足够多的 10G 以太网交换机。rnrn (7) 开源的 Linux 操作系统。rnrnrn* 系统运行的环境rnrn 以下条件假设已经或将会存在,即我们不在我们考虑之列。rnrn (1) 设备间空间够大,电力充足 :)rnrn (2) 支持 1G 用户同时访问的足够的外网带宽,可能是很多路宽带接入,包括 DNS 负载均衡。rnrn (3) 全部 IPv6。如果有 IPv4 接入,前端接 IPv6/IPv4 NAT 转换器吧。rnrn (4) 有足够好足够多的的 TCP 连接负载均衡器,能把 1G 个 TCP 连接分摊到上万块网卡上。rnrnrn* 一个应用!rnrn 在开始设计之前,请先明白一个核心问题,我们的系统只处理一个应用!这意味着这是一个精简到最小的不能再分解的应用。rnrn 举例说明一:假设应用是一个大论坛,由很多子论坛,每个子论坛相对独立。这个应用可以不用我们的系统,因为每个自论坛可以单独的使用简单的 HTTP Server。当然,如果因为某种原因,比如统一验证身份 Session,或复杂的内部结算交易,使用我们的系统是合适的。rnrn 举例说明二:假设应用是一个多人实时绘画系统,支持 10 亿人同时在一张纸上画画。这个应用就很难再分解,我们的系统适合于这种应用。rnrn 哦,不要笑话某些想法的疯狂,我们就是要支持更加疯狂的公益创意或商业创意,能够付诸实现 :)rnrnrn* TCP 连接rnrn 在网卡的驱动程序里处理。即不进入内核的 TCP/IP 协议栈,也没有某个进程在阻塞在 socket 的 listen() 中。rnrn 所以每太 PC 中需要有“正常”的网卡,也有专用的“数据网卡”。我们为“数据网卡”设计单独的驱动程序。rnrn “数据网卡”驱动程序从以太数据包中直接判断,如果是 TCP 连接请求,直接返回 accept。rnrn “正常”网卡的数据处理过程不做任何修改。我们可以通过“正常”网卡登录到 PC 的 Linux 上,进行管理。rnrnrn* 身份认证rnrn 用户身份认证包括首次认证,即用户名/密码检验;以及 HTTP Session 验证。rnrn 我们有 1G 用户,假设每个用户占用 4K 字节(一页),包括用户名、密码、该用户当前所有的 Session/TCP 连接信息、TCP 输入数据缓冲区、TCP 输出信息,以及一些附加信息,等等。总共需要 4TB 内存来存放所有的用户信息。rnrn 为了实现高性能,系统运行中所有数据均存放在内存,为了便于统一存取,我们把 10TB 的内存设计在一个连续的内存地址空间内,即内部数据引用都用 64 位的地址指针来表示。rnrn 每台 PC 大约有 10G 内存,所以 4TB 的用户数据存放在 400 台 PC 上。1G 用户用 32 位二进制数就可以定位,所以 1G 用户信息连续的存放在 4TB 空间内,用 44 位内存地址来定位。rnrn 用户名/密码检验时,把用户的用户名用 Hash 算法翻译成一个地址指针,然后去内存中连续的小范围内查找用户,找到后,检查用户名/密码。检查通过后,把该用户的 32 位地址,再加上一个大随机数,合并作为 Session ID。后续的 Session 检验就简单了。为了便于提高性能,可以把 SessionID 放在 URL 中,关于这个后续版本再讨论。rnrn 所以,当一个 PC 的“数据网卡”收到一个用户登录请求后,相应的 CPU Core 首先计算用户的 32 位地址,如果这个地址不在本 PC,那就把这个请求发送给那个地址所在的 PC。rnrn 出于性能的考虑,在设计网页时,一次提交 Form 的数据的不要超过一个以太网数据包的容量(约 1.4KB),这也是向客户端发送的 TCP 窗口尺寸。接收 Upload 文件的情况留待后续设计考虑。rnrn 所以,在每个 PC 中的 CPU Core 分成两种,一个是“正常”的 CPU Core,其他的是“数据 CPU Core”,“数据 CPU Core” 只处理“数据网卡”的收发数据任务。“数据 CPU Core” 工作在内核级,主要是为了高速度读写网卡,如果进出内核的开销能够忍受的话,也可以工作在用户级。工作在内核级的“数据 CPU Core”,为了不影响正常的操作系统内核数据,可以设定单独的内存映射,内存页目录页表,即不能访问正常的操作系统数据。当然“数据 CPU Core” 不参与正常的操作系统任务调度。“数据 CPU Core” 有自己的调度方式。在“正常 CPU Core” 空闲的时候,也可以把它暂时设定成“数据 CPU Core”。rnrn rn* 解析 URL,找到 Objectrnrn 出于性能的考虑,系统中的所有 Object 都保存成一块静态内存数据区,通过地址指针来访问。rnrn 系统中根据用户提交的数据计算而成的结果,也保存成静态内存数据区。比如论坛里提交的帖子;博客里提交的文章;博客里的评论;实时绘图绘制出来的图像等。实时图像的数据区过期后可以重复使用。rnrn 静态的文件,从硬盘上预读入内存。rnrn 如果应用是大数据文件,比如电影,则适当增加全系统的内存,或者按照一定的算法做硬盘缓冲,或者限制用户的数据流量(只要保证电影连续播放),来减少带宽和内存占用。关于这个问题留待以后设计讨论。rnrn 所以系统中的 Object,可以嵌套包含 Object。rnrn 每个 Object 对应的 URL,在明确可预期是静态数据的情况下,可以用地址指针来做 URL,即使系统重新启动,重新加载所有数据也保持地址不变。如果 Object 是动态的,可以用字符串或者字符窜加 ID 来表示。rnrn 所以整个系统的所有 Object,也是统一的放在一个巨大的地址空间中。另一方面,Object 的总量是有限的,1G 的用户存取的很多内容是重复的,往往很多人同时访问的数据量并不会很大,比如商品股票交易数据、热门的影视作品。而像商品股票交易数据重点需要做好存储,不一定是大量历史数据的实时访问。如果很多人同时访问大量的数据,比如全球精细地图系统、人类拍摄过的所有电影高清晰版全库,则数据流量不会很大,而且是静态数据,每个用户的带宽可以限制在一定范围内,所以可以采用硬盘数据缓冲机制。rnrn 所以整个系统中,可以用 5TB 来保存所有 Object,每个 Object 通过指针地址来访问。所有的 Object 分布在超过 500 台 PC 上。考虑到访问量大的 Object,应该在多个 PC 上保存副本,以实现高性能。此处负载均衡的问题留待以后设计讨论。rnrn 关于访问权限,某个用户是否有访问某个 Object 权限的问题,可以设计一定格式一定数量的“授权码”,每个需要被保护的 Object 都给定一个或几个“授权码”。这些授权码还可以组成角色,或多级(多层)角色。用户可以被设定成多种角色,以此来实现大范围的授权。至于 Object 对单个用户授权,则可以在 Object 中记录用户的 ID(地址指针),比如博客内容只能由博客作者自己来修改。rnrn 权限的内存占用设计在 1MB 到 100MB 之间。关于这个后续设计可以继续讨论。rnrnrn* 计算 Objectrnrn 在此特制计算量大的 Object,比如实时绘图,或者商品交易撮合。rnrn 计算过程如果可以并行分解后合成,比如实时绘图,那就分解到若干个 CPU Core 上进行计算,计算后发送到一个 CPU Core 上合成。如果计算过程不能并行分解,比如某单一产品交易撮合,那就在一个 CPU Core 上计算。rnrn 计算的结果保存成静态数据。如果需要复制到多个 PC 上,则通过以太网广播。如果广播太多影响性能,可以再划分子网。rnrnrn* 将 Object 数据发给用户rnrn 出于性能的考虑,不使用输出缓冲,即不在全局内存间复制数据作为输出缓冲。rnrn 为每个 Object 设定一个输出 TCP 连接池,池中的每个连接数据是 TCP 连接的发送状态。完整的 HTTP 响应数据就通过这里发送回去,即发送静态 Object 的一部分数据,大小是以太网数据包尺寸与用户 TCP 接受窗口尺寸的相比取小,以太数据包里面打成IP包,包括服务器端的公共 IP 地址,准备好后扔给路由器,发送给用户。rnrn 这部分 TCP 连接池的内存是动态分配的,大小接近 1TB,分摊到每个 Object 所在的 PC。内存分配管理方法可以类似 Linux 内核的 Slab,整页分配。因为 TCP 的发送时间可以设定超时,所以可以保证这些内存会在一定时间内重新投入使用。rnrnrn* 一个典型的 HTTP 请求/响应过程rnrn (1) 用户正在使用浏览器/聊天工具之类的客户端,发 TCP 请求(一个 IP 包)到系统,首先到达负载均衡器。rnrn (2) 负载均衡器将 TCP 请求发给一台 PC 的“数据网卡”,“数据网卡”的驱动程序直接回复,建立 TCP 连接。rnrn (3) 用户端发过来一个 HTTP POST 请求(假设不超过一个以太数据包),负载均衡器将此请求发给用户管理 PC 的“数据网卡”。rnrn (4) 用户管理 PC 内的一个“数据 CPU Core”解析 Session ID,发现不是自己的用户,于是将此请求转发给第二个用户管理 PC。rnrn (5) 第二个用户管理 PC 检验用户的 Session ID 正确后,解析 URL 得到 Object ID,然后按负载均衡算法将此请求转发给某个 Object 管理 PC 的“数据网卡”。rnrn (6) Object 管理 PC 的“数据 CPU Core” 解析用户的 POST Body,计算并更新 Object 的静态数据。rnrn (7) Object 管理 PC 把更新过的静态数据广播发送其他的 Object 管理 PC。rnrn (8) Object 管理 PC 在此 Object 的 TCP 连接池中增加一个连接记录。rnrn (9) Object 管理 PC 通知负载均衡器,此 TCP 连接的后续 IP 包都发到本机,最好发到本网卡。rnrn (10) Object 管理 PC 从静态数据中取出一部分,打包成包含 IP 包的以太网包,IP 包的目的地址是用户的 IP 地址,发给路由器。rnrn (11) 用户收到 IP 包后,发回接收确认 ACK 包,负载均衡器将此包发给 Object 管理 PC,Object 管理 PC 继续打包并发送下一个数据包。rnrn (12) Object 管理 PC 发送全部数据,或者连接超时,从连接池中删除此连接记录。如果一个页面中的所有连接记录都被删除,则回收此连接池。rnrn (13) 一个大小为 10KB 的 Object 请求/响应能在 1 秒钟内走完上述过程,在同时有 1G 个请求的情况下。rnrnrn* 遗留问题rnrn 本版设计先不考虑下列问题,后续版本会加入。rnrn (1) SSL/TLSrnrn (2) 热插拔/容错rnrn (3) 数据在硬盘上如何存放,以及是否使用数据库。rnrn (4) 内部负载均衡rnrn (5) 内部网络数据转发丢失问题rnrn (6) CPU Cache 优化rnrn

没有更多推荐了,返回首页

私密
私密原因:
请选择设置私密原因
  • 广告
  • 抄袭
  • 版权
  • 政治
  • 色情
  • 无意义
  • 其他
其他原因:
120
出错啦
系统繁忙,请稍后再试