网站架构模式

为了解决高并发访问、海量数据处理、高可靠运行等一系列问题,实现高性易伸缩、可扩展、安全。可以使用以下各种方式进行架构网站。

1、 分层

分层是一种常见的架构模式,将系统在横向维度上切分成几个部分,每个部分负责一部分相对比较单一的职责,然后通过上层对下层的依赖和调用组成的一个完整系统。
分层结构无处不在,比如网络的7蹭通信协议等。我们可以将网站分为应用层服务层数据层

  • 应用层:负责具体业务和视图展示。如网站首页、商品详情页等。
  • 服务层:为应用层提供服务支持。如用户管理服务、购物车服务、支付服务等。
  • 数据层:提供数据存储访问服务。如数据库、缓存、文件、搜索引擎等。

通过分层,可以将一个网站切分不同的部分,便于分工合作开发维护,职责清晰。每层之间具有一定的独立性,他们之间也需要严格遵循一定官则,如禁止跨层访问、逆向调用。

在具体开发中,大的分层基础上还可以继续分层。如应用层可以在细分为视图层和业务逻辑层;服务层也可以分为数据接口层和逻辑处理层等。

2、 分割

分层是对系统进行横向切分,那么分割就是将系统进行竖向切分。

网站越大,功能越复杂,服务和数据处理的种类也越多,将不同的功能和服务分割开来,包装成高内聚低耦合的模块,便于分布式部署,提高网站并发处理能力和功能扩展能力。

在应用层,将不同的业务进行分割。比如将购物、论坛、搜索、广告分成不同的模块,由独立的团队负责。
同分层一样,在大的分割基础上还可以继续进行分割。比如购物模块可以进一步分割为机票酒店业务、3C业务等更细小的颗粒度。这些模块不管在逻辑上还是物理部署上都是独立的,相应的服务层也可以更具需要将服务分割成适合的模块。

3、 分布式

分层和分割的主要目的就是为了切分后的模块便于分布式部署。分布式之后就意味着拥有更多的cpu、内存、磁盘存储资源,也就是能够更好的处理高并发和大数据。

但是相对的也会带来其他问题。首先,分布式意味着服务之间调用必须使用网络,这就可能会对性能造成一定影响。其次,服务器越多,服务器宕机概率也就越大。另外,数据在分布式环境中保持一致性也越困难,分布式事务也难以保证,对网站业务的正确性和业务流程有可能造成很大的影响。最后,分布式还导致网站依赖错综复杂,开发管理维护困难性提高。

因此分布式设计要根据具体的情况量力而行,切莫为了分布式而分布式,但是对于大型网站来说,分布式是必须走的路。

常用的分布式方案有以下几种:

  • 分布式应用和服务
  • 分布式静态资源
  • 分布式数据和存储
  • 分布式计算
  • 当然还有分布式配置、分布式锁、分布式文件系统等。

4、 缓存

缓存是改善系统性能的第一手段,在现代网站架构中缓存无处不在,下面介绍几种常用的缓存手段。

  • CDN
    即内容分发网络,将某些资源(如静态资源)部署在距离终端用户最近的网络服务商,当用户请求到达网络服务商这里时,这些资源就会立刻返回给用户,减去了大量长距离网络传输的性能消耗。

  • 反向代理
    反向代理属于网站前端架构的一部分,部署在网站的前端,当用户请求到达反向代理服务器时,这里缓存的静态资源可以直接返回给用户。当然反向代理不仅仅是这点功能而已。

  • 本地缓存
    不多说了这个,本地访问的数据~~~~

  • 分布式缓存
    大型网站数据量非常庞大,及时是一小部分的缓存,也不是单机能够承受的。

使用缓存有2个条件,一个是数据访问热点不均衡,某些数据会被更频繁的访问,这些数据应该放到缓存中。
第二个是某段时间内不会过期的数据。

5、 异步

大型网站中系统解耦的手段除了分层、分割,还有一个重要的手段 —— 异步。异步架构是典型的生产者消费者模式,两者间不存在直接调用,只要保持数据结构不变化,彼此功能实现可以随意变化而互不影响,这对网站扩展非常便利。
异步消息有几点很重要好的特性:

  • 提高系统可用性
    消费者服务器发生故障,数据仍旧会保存在消息队列服务用,生产者服务器依旧可以处理业务请求,只需要等待消费者服务器正常后继续工作。
  • 加快网站相应速度
    某些业务可以延时处理,减少生产者响应时间
  • 消除并发访问高峰
    消息队列可以消峰填谷这点非常重要,比如网站出小活动,造成网站并发陡增,这可能造成玩网站负载过重,响应延迟,甚至宕机。消息队列可以将数据保存在队列中,等待消费者服务器依次处理,就不会对网站造成太大压力。

需要注意的是,异步方式会对业务处理造成影响,需要网站产品设计方面支持。

6、 冗余

现在互联网公司都是7×24小时连续运行,但是服务器随时可能出现故障,特别是服务器规模较大时,出现某台服务器宕机是必然的事情。如果我们对服务器进行了冗余,那么就可以不受宕机影响,网站依然可以运行,不丢失数据。

冗余可以实现高可用

数据库除了定期备份,存档保存,实现冷备份外、为了业务高可用,还需要对数据库进行主从分离,实时同步热备份。甚至为了抵御地震、海啸等不可抗力,需要在全球范围部署容灾数据中心。

7、 自动化

  • 自动化部署 不对说了
  • 自动化监控 对服务进行心跳检测,并监控其各项性能指标和应用程序的关键数据指标
  • 自动化报警 通过自动话监控发现异常,就需要向相关人员发送警报信息
  • 自动化失效转移 将失效服务器从集群中移除
  • 自动化失效恢复 故障消除后,将服务器自动添加入集群,同步数据保证一致性
  • 自动化降级 当网站遇到访问高峰时,超出网站最大处理能力,为了保证整个网站的可用性,拒绝或关闭部分无关紧要的服务,保证网站主要流程能够运行流畅,将系统负载降至安全水平。
  • 自动化分配资源 当网站遇到访问高峰时,已经自动化降级的同时,必要时,可以通过将空闲资源分配给重要服务,扩大其部署规模

8、 安全

互联网的开放特性,使得其从诞生起就面对巨大的安全挑战,网站在安全架构方面也积累了许多模式:

  • 通过密码和手机校验码进行身份认证
  • 登录、交易等操作需要对网络进行加密,其他敏感信息也需要进行加密
  • 为了防止机器人滥用网络资源攻击,网站使用验证码识别
  • 对于常见的攻击手段xss攻击、sql注入,对表单提交做相应的编码转换处理
  • 对垃圾信息、敏感信息做相应的屏蔽
  • 对交易转账等重要操作,根据交易模式和交易信息进行风险控制

事务ACID —— 隔离性

什么是隔离性 ?

当两个或者多个事务并发访问(此处访问指查询和修改的操作)数据库的同一数据时所表现出的相互关系。
引用自 维基百科 —— ACID

多个事务单元之间的关系可以是以下四种:

  • 读读
  • 读写
  • 写读
  • 写写

序列化读写(Serializable)

上一篇事务ACID —— 一致性中说到,对同一数据访问时,为了强一致性的表现,就利用排他锁把一个有一个的事务排成一个队,这就是序列化读写级别

这个时候数据是非常安全的,不会出现混乱的情况。但是在这个级别的时候就会出现一个很严重的问题,没有并发,系统性能差。

可重复读(repeatable read)

为了解决序列化读写级别性能差,那么就有人想出来了,把排他锁变为读写锁。那么就可以提升系统的性能。

可重复读级别就是把序列化读写级别排他锁变为读写锁(读锁不能升级为写锁),这样就实现了读读并行。

可以从图二中看出,相对于图一,此时读读已经并行了。

读已提交(read committed)

为了进一步提升性能,人们就把可重复读级别读写锁(读锁不能升级为写锁)变更为读写锁(读锁可以升级为写锁)

那么我们可以看到下图,并发有进一步提升了。此时就实现了读读、读写并行(写读还不能并行)。

读未提交(Read uncommitted)

当只加写锁不添加读锁时候,看下图可以发现,读的顺序被上升了一格。

这种情况下,会出现幻读和脏读情况(读到写事务未提交的数据,写事务可能会出现回滚,那么读到的数据就是不存在的一个数据)。

总结

其实我们发现,隔离性的4中级别就是各种锁的不同组合变成了不同的隔离级别。
重另一个方面来说,隔离性就是以性能为借口,对一致性的一种破坏。

扩展,快照隔离级别(sanpshot isolation)/多版本并发控制(mvcc)

快照隔离级别的核心思路就是无锁编程,·copy on write·。

简单的说,将每次更新记录到一个回滚段之中。在写事务发生时,读事务进来,可以直接进入回滚段中读取数据。它的并行度可以达到读未提交级别,数据安全性可以达到可序列化读写级别

但是它也有一个缺点,就是会产生大量的二进制日志。

事务ACID —— 一致性

什么是一致性 ?

在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的默认规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
引用自维基百科 —— ACID

上一篇文章中事务ACID —— 原子性,原子性的特性是要么成功,要么失败,不存在中间状态。

那么一致性的核心是什么?

一致性的核心就是一个词happen-before。简单的说,就是can

假设 事务A事务B的关系是前后关系,那么这就没有任何问题。
但是所有事务单元不可能是完美意义上的先后关系,它一定会存在不同意义上的并发。
当2个事务单元并发时怎么办?

继续拿上一边文章中的A 和 B转账案例来说,

假设 事务A 是上边这个操作,当事务A执行到ver2时,A 和 B 都是 0 元, 此时 事务B 为 C 给 B 转账了300元,那么 B 就应该有300 元了。就在这个时候 事务A执行失败,事务为了保持原子性,操作回滚,那么B的钱就滚回了0元,也就是B资损了300元。那么也就出现了金钱不一致的情况。

那么最简单的方法就是加锁,让事务B不影响事务A的执行结果。如下:

有了这个锁以后,事务B就会形成要么在事务A之前执行,要么在其之后执行。这就是一致性给到的保障。这样做以后我们就会发现,这个其实就是把所有的请求做了一个排队的处理。但是它有一个致命的缺点就是它的并发是起不来的,那么性能也就是极差的。

由此我们可以总结,一致性就是一个事务单元只有在全部成功后,才可以对外可见。拿转账业务来说就是要么A有100元,要么B有100元。当ver2时,A和B都为0元是对外不可见的,外部是访问不到的。

事务ACID —— 原子性

什么是原子性 ?

一个交易(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
引用自 维基百科 —— ACID

即一个事务我要么成功,要么失败,没有中间状态。

如何回滚 ?

场景: A 对 B 进行转账

以上步骤,假设说在ver3为B加上100元时发生错误。
如果没有事务那么系统数据就会停留在ver2这里,那么就会造成A减掉了100元,而B也没有拿到这个100元,造成了用户资损。
添加上事务后,那么系统就会回滚到ver1,数据还原成了初始状态。

那么事务失败,需要数据回滚。数据库是如何实现的呢?

其实数据库在事务开启后,当我们做的每一次变更,系统就会自动创建一个undo。那么需要回滚时候,数据库就会按照undo来进行回滚。如下:

php-socket 实现简单服务器

Socket基础知识

我们可以把Socket当做是一种数据结构,客户端和服务器间通过这种数据结构来交换数据。服务器开始监听连接,当客户端想要连接服务器时,会通过服务器监听的端口开启一个会话,服务器收到客户端的请求后,建立连接完毕,然后继续监听下一次连接。

要产生一个Socket,你我们需要三个变量:一个协议(protocol)、一个socket类型(socket type)和一个公共协议类型(common protocol type)。下面将会详细介绍各个部分的具体内容。

协议

  • AF_INET:这是大多数用来产生socket的协议,使用TCP或UDP来传输,用在IPv4的地址
  • AF_INET6:与上面类似,不过是来用在IPv6的地址
  • AF_UNIX:本地协议,使用在Unix和Linux系统上,它很少使用,一般都是当客户端和服务器在同一台及其上的时候使用

Socket类型

  • SOCK_STREAM:这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
  • SOCK_DGRAM:这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
  • SOCK_SEQPACKET:这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
  • SOCK_RAW:这个socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
  • SOCK_RDM:这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序

公共协议

  • ICMP:互联网控制消息协议,主要使用在网关和主机上,用来检查网络状况和报告错误信息
  • UDP:用户数据报文协议,它是一个无连接,不可靠的传输协议
  • TCP:传输控制协议,这是一个使用最多的可靠的公共协议,它能保证数据包能够到达接受者那儿,如果在传输过程中发生错误,那么它将重新发送出错数据包。

socket示例

stream示例

附:
socket系列方法

  • socket_accept() 接受一个Socket连接
  • socket_bind() 把socket绑定在一个IP地址和端口上
  • socket_close() 关闭一个socket资源
  • socket_connect() 开始一个socket连接
  • socket_create_listen() 在指定端口打开一个socket监听
  • socket_create() 产生一个socket,相当于产生一个socket的数据结构
  • socket_get_option() 获取socket选项
  • socket_getpeername() 获取远程主机的ip地址
  • socket_getsockname() 获取本地socket的ip地址
  • socket_listen() 监听由指定socket的所有连接
  • socket_read() 读取指定长度的数据
  • socket_readv() 读取从分散/聚合数组过来的数据
  • socket_recv() 从socket里结束数据到缓存
  • socket_recvfrom() 接受数据从指定的socket,如果没有指定则默认当前socket
  • socket_recvmsg() 从iovec里接受消息
  • socket_select() 多路选择
  • socket_send() 这个函数发送数据到已连接的socket
  • socket_sendmsg() 发送消息到socket
  • socket_sendto() 发送消息到指定地址的socket
  • socket_set_block() 在socket里设置为块模式
  • socket_set_nonblock() socket里设置为非块模式
  • socket_set_option() 设置socket选项
  • socket_shutdown() 这个函数允许你关闭读、写、或者指定的socket
  • socket_write() 写数据到socket缓存

stream系列方法

  • stream_bucket_append函数:为队列添加数据 
  • stream_bucket_make_writeable函数:从操作的队列中返回一个数据对象
  • stream_bucket_new函数:为当前队列创建一个新的数据
  • stream_bucket_prepend函数:预备数据到队列 
  • stream_context_create函数:创建数据流上下文
  • stream_context_get_default函数:获取默认的数据流上下文
  • stream_context_get_options函数:获取数据流的设置
  • stream_context_set_option函数:对数据流、数据包或者上下文进行设置
  • stream_context_set_params函数:为数据流、数据包或者上下文设置参数
  • stream_copy_to_stream函数:在数据流之间进行复制操作
  • stream_filter_append函数:为数据流添加过滤器
  • stream_filter_prepend函数:为数据流预备添加过滤器
  • stream_filter_register函数:注册一个数据流的过滤器并作为PHP类执行
  • stream_filter_remove函数:从一个数据流中移除过滤器
  • stream_get_contents函数:读取数据流中的剩余数据到字符串
  • stream_get_filters函数:返回已经注册的数据流过滤器列表
  • stream_get_line函数:按照给定的定界符从数据流资源中获取行
  • stream_get_meta_data函数:从封装协议文件指针中获取报头/元数据
  • stream_get_transports函数:返回注册的Socket传输列表
  • stream_get_wrappers函数:返回注册的数据流列表
  • stream_register_wrapper函数:注册一个用PHP类实现的URL封装协议
  • stream_select函数:接收数据流数组并等待它们状态的改变
  • stream_set_blocking函数:将一个数据流设置为堵塞或者非堵塞状态
  • stream_set_timeout函数:对数据流进行超时设置
  • stream_set_write_buffer函数:为数据流设置缓冲区
  • stream_socket_accept函数:接受由函数stream_ socket_server()创建的Socket连接
  • stream_socket_client函数:打开网络或者UNIX主机的Socket连接
  • stream_socket_enable_crypto函数:为一个已经连接的Socket打开或者关闭数据加密
  • stream_socket_get_name函数:获取本地或者网络Socket的名称
  • stream_socket_pair函数:创建两个无区别的Socket数据流连接
  • stream_socket_recvfrom函数:从Socket获取数据,不管其连接与否
  • stream_socket_sendto函数:向Socket发送数据,不管其连接与否
  • stream_socket_server函数:创建一个网络或者UNIX Socket服务端
  • stream_wrapper_restore函数:恢复一个事先注销的数据包
  • stream_wrapper_unregister函数:注销一个URL地址包