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(及其上的应用系统)共同组成。留待后续版本考虑。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值