这一生听过许多道理,但仍是过欠好这一生,这是由于短少真正的着手实践,光听道理,短少着手实践的进程,学习难免会让人觉得味同嚼蜡,所以我的共享都比较倾向于实践,在一次次着手实践的进程中感触常识本来纯真的容貌。

大家好,我是蓝胖子,往往从事互联网开发的同学都听过cdn这个词,不过对于刚入行的同学或许会对这个概念比较模糊,今日咱们就来聊聊它,而且我会在原理的基础上在本地建立一个cdn环境,模仿域名装备,回源,以及缓存的进程。

本节源码现已上传到github

https://github.com/HobbyBear/cdndemo

现在,让咱们开端吧。

cdn原理介绍

首先,咱们来看下为什么要用cdn,比方一个专注做视频播映或者图片阅览的网站,当用户浏览网站时,需求从网站拉取图片或者视频资源,这将发生流量费用,而且,如果用户里网站服务器越远,发生的流量费用将越高。而cdn的原理则是将视频或者图片资源缓存在离用户比较近的服务器上,这样既提升了呼应速率,又节约了流量费。

来看下使用cdn后,用户拜访网站的进程。

cdn原理分析-本地搭建cdn模拟访问过程

如上图所示,假定用户自己的想要加快的域名是web.cdn.test,如果用户想对这个域名进行加快,首先要去cdn服务商那里装备上这个加快域名源站服务地址,接着cdn服务商会生成一个域名地址,这儿假定为web.cdn.test.c.lanpangzi,而用户自己需求去自己的dns服务商那里将加快域名web.cdn.test 指向这个新的域名地址,这种将一个域名指向另一个域名的记载被称作cname记载。

经过上述步骤后,对域名web.cdn.test的拜访会被指向web.cdn.test.c.lanpangzi,但现在还有一个问题,新的域名web.cdn.test.c.lanpangzi应该由cdn服务商自己的dns调度体系去解析,这样cdn服务商才干将边际节点的ip返回给用户,那本地dns服务器怎么知道web.cdn.test.c.lanpangzi这个域名要交给哪台机器去解析呢?

原因是这样的,cdn服务商在注册自己的主域名时(这儿的主域名是langpangzi.) ,向dns服务器注册了一条ns记载,ns记载能够指定将某个域名的解析交给哪一台dns服务器解析,比方这儿,cdn服务商就把c.langpangzi. 域名的解析指向了cdn服务商自己的dns服务器。

完成了这一步,用户自己的加快域名最终就会被cdn服务商的dns服务器去解析了,cdn服务商一般在全球各地都有自己的节点,所以它会根据用户的ip去挑选一个离用户比较近节点ip返回,这些节点被称作边际节点 ,这样用户的恳求就能就近拜访了。

本地建立一个cdn

咱们把上面的恳求进程与cdn架构在本地模仿下,把自己幻想成一个cdn服务商,咱们将会建立cdn的域名调度中心,和边际节点,然后允许用户供给加快域名和回源服务器给我进行装备。

建立dns服务器

咱们先来建立一个dns服务器,由于无论是cdn仍是用户自身都需求进行一些dns域名装备,你能够把当前这个dns服务器幻想成第三方dns运营商。

咱们用dnsmasq在本地进行dns服务器的建立,我本地机器用的mac,装置指令如下:

brew install  dnsmasq
==> Dependencies
Build: pkg-config ✔
==> Caveats
To start dnsmasq now and restart at startup:
  sudo brew services start dnsmasq
Or, if you don't want/need a background service you can just run:
  /opt/homebrew/opt/dnsmasq/sbin/dnsmasq --keep-in-foreground -C /opt/homebrew/etc/dnsmasq.conf -7 /opt/homebrew/etc/dnsmasq.d,*.conf
==> Analytics
install: 0 (30 days), 0 (90 days), 0 (365 days)
install-on-request: 0 (30 days), 0 (90 days), 0 (365 days

接着需求对其装备文件进行修正,装备上游服务器,以及装备当前dns服务器能够解析的域名

从装置的信息能够看出,其装备文件是在/opt/homebrew/etc/dnsmasq.conf 这个位置,这儿我直接给出我的装备信息

# 装备上行DNS,对应no-resolv
resolv-file=/Users/xiongchuanhong/dnsmasq.conf
# 运行进程以哪个用户身份运行,直接用root,由于dnsmasq的装备涉及到权限问题
user=root
# 装备dnsmqsq运行日志,对排错很重要
log-facility=/Users/xiongchuanhong/logs/dnsmasq.log
# resolv.conf内的DNS寻址严厉按照从上到下顺序执行,直到成功停止
strict-order
# DNS解析hosts时对应的hosts文件,对应no-hosts
addn-hosts=/etc/hosts
cache-size=1024
# 多个IP用逗号分隔,192.168.x.x表示本机的ip地址,只要127.0.0.1的时分表示只要本机能够拜访。
# 经过这个设置就能够完成同一局域网内的设备,经过把网络DNS设置为本机IP从而完成局域网范围内的DNS泛解析(注:无效IP有或许导至服务无法发动)192.168.17.150 是我本地机器内网ip
listen-address=127.0.0.1,192.168.17.150
# 相当于ns记载,c.lanpangzi的域名以及其子域名都会由本地1053端口的进程去进行解析
server=/c.lanpangzi/127.0.0.1#1053
# cname记载,拜访web.cdn.test 的域名都会指向web.cdn.test.c.lanpangzi 
cname=web.cdn.test,web.cdn.test.c.lanpangzi
# 重要!!这一行便是你想要泛解析的域名装备.
#address=/hello.me/127.0.0.1

上面resolv-file 所指向的文件是要装备的上游dns服务器,/Users/xiongchuanhong/dnsmasq.conf装备如下:

(base) ➜  ~ cat /Users/xiongchuanhong/dnsmasq.conf
 nameserver 8.8.8.8

当本地解析不了域名,那么dnsmasq会问询它的上游dns服务器,所以咱们把它装备成谷歌的域名解析体系。

留意Users/xiongchuanhong/dnsmasq.conf和Users/xiongchuanhong/logs/dnsmasq.log文件都需求用root用户去创立,否则到时分运行会报权限过错。

接着发动dnsmasq服务

sudo brew services start  dnsmasq

然后再把机器的dns服务器ip改成127.0.0.1

cdn原理分析-本地搭建cdn模拟访问过程

cdn原理分析-本地搭建cdn模拟访问过程

接着测验下修正后的网络拜访有没有问题

(base) ➜  ~ ping www.baidu.com
PING www.a.shifen.com (14.119.104.189): 56 data bytes
64 bytes from 14.119.104.189: icmp_seq=0 ttl=54 time=36.413 ms
64 bytes from 14.119.104.189: icmp_seq=1 ttl=54 time=11.351 ms
64 bytes from 14.119.104.189: icmp_seq=2 ttl=54 time=11.339 ms
64 bytes from 14.119.104.189: icmp_seq=3 ttl=54 time=16.660 ms

域名解析正常,接着进行下面的步骤。

建立回源服务器和cdn边际节点

接着咱们来快速建立下回源服务器和cdn的边际节点,这儿我是直接借用了nginx来进行建立,由于nginx能够用作静态资源服务器,所以能够直接发动一个nginx容器把它作为源服务器,供给图片资源的拜访。 而且nginx也能供给缓存服务,这也刚好契合边际节点缓存资源的需求。由于边际节点需求回源到源服务器,但是容器的ip又是不固定的,所以这儿我直接用docker-compose对这两个容器进行了编排,这样在写nginx装备文件的时分,便能够用容器名替代ip了。

docker-compose.yaml装备文件

version: "3"
services:  
  sourceserver:  
    image: nginx:latest  
    container_name: source-server  
    hostname: source-server  
    ports:  
      - '8080:80'  
    volumes:  
      - ./sourceserver/nginx.conf:/etc/nginx/nginx.conf  
      - ./sourceserver/logs:/var/log/nginx  
      - ./sourceserver/imgs:/imgs  
  edgenode:  
    image: nginx:latest  
    container_name: edgenode  
    hostname: edgenode  
    ports:  
      - '80:80'  
    volumes:  
      - ./edgenode/nginx.conf:/etc/nginx/nginx.conf  
      - ./edgenode/logs:/var/log/nginx  
      - ./edgenode/cache:/cache

源站服务器的nginx装备


worker_processes  1;  
error_log   /var/log/nginx/error.log;  
events {  
    worker_connections  1024;  
}  
http {  
    default_type  application/octet-stream;  
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '  
                      '$status $body_bytes_sent "$http_referer" '  
                      '"$http_user_agent" "$http_x_forwarded_for"';  
    # 静态资源装备  
    server {  
        listen       80;  
        access_log  /var/log/nginx/access.log  main ;  
        location /static {  
            alias   /imgs;  
        }  
    }  
}

边际节点的nginx装备

在server模块里,咱们装备 proxy_pass 转发途径时,直接用源服务器的容器名替代了ip,http://sourceserver。当本地没有缓存时,就会经过http://sourceserver 拜访源站服务器。

留意下,在真实环境里,边际节点的源站服务器名和加快域名必定不是写死的,是用户装备在cdn服务商的数据库里,然后边际节点再从服务器里读取的。

而且边际节点要求只要用户的加快域名才干拜访,所以在转发恳求前还要判别下域名是不是用户的加快域名。

worker_processes  1;
error_log   /var/log/nginx/error.log;  
events {  
    worker_connections  1024;  
}  
http {  
    log_format  main  '$host- $remote_addr - $remote_user [$time_local] "$request" '  
                      '$status $body_bytes_sent "$http_referer" '  
                      '"$http_user_agent" "$http_x_forwarded_for"';  
      proxy_cache_path  /cache levels=1:2 keys_zone=lanpangzi-cache:20m max_size=50g inactive=168h;  
      proxy_cache lanpangzi-cache;  
      proxy_cache_valid 168h;  
  server {  
      root /static;  
      listen       80;  
      location ~* .(css|js|png|jpg|jpeg|gif|gz|svg|mp4|ogg|ogv|webm|htc|xml|woff)$ {  
        if ($host != 'web.cdn.test')  {  
             return 403;  
           }  
      access_log  /var/log/nginx/access.log  main;  
      proxy_pass http://sourceserver;  
    }  
  }  
}

这样就快速建立好了源服务器和边际节点,咱们到时分直接用docker-compose up指令便能够发动容器,边际节点监听了本地80端口。

建立cdn的域名调度体系

最终,咱们来建立下cdn的域名调度体系,避免忘记,咱们再来串联下装备cdn的进程,用户拥有自己的加快域名和源站服务器,而且将这两个告诉了cdn服务商,cdn服务商就发生一个新域名,这个新域名需求装备为cname记载,与用户的加快域名相关起来,这条cname记载作咱们在建立dnsmasq时现已写死在了装备文件里。

cname=web.cdn.test,web.cdn.test.c.lanpangzi

留意,实际中,cdn服务商新生成的域名需求用户自己去其dns服务商那里去进行装备

一起,cdn服务商生成的新域名解析由于装备的ns记载,现已将新的这个域名解析指向了自己的dns域名调度体系,到时分经过这个体系便能够返回给用户最近的边际节点的ip。

这条ns记载咱们也写死在了dnsmasq的装备文件里。

server=/c.lanpangzi/127.0.0.1#1053

到时分咱们本地会在1053端口发动一个进程用于对c.lanpangzi域名进行解析。

接着来思考下如何写一个域名调度体系,咱们的服务通信需求遵循dns协议,由于咱们的边际节点是布置到本地,ip是127.0.0.1,所以当解析到恳求时要问询web.cdn.test.c.lanpangzi这个域名时,直接返回本地这个ip即可。 那对于其他域名如何处理,当然是直接丢掉,由于cdn的域名调度体系,只供给对自己cdn服务商生成的域名进行解析。

用golang简略完成代码如下:

这儿我直接在代码里将cname域名以及边际节点的ip写死在代码里了(重在模仿cdn恳求进程),只要是web.cdn.test.c.lanpangzi.这个域名就返回边际节点ip,由于咱们都是布置到本地,所以边际节点ip便是127.0.0.1了,dns的解析用了现成的golang库。

import (
   "github.com/miekg/dns"  
   "log"   "net")  
var cdnConfig = map[string]string{  
   "web.cdn.test.c.lanpangzi.": "127.0.0.1",  
}  
// 处理到来的恳求  
func handler(writer dns.ResponseWriter, req *dns.Msg) {  
   var resp dns.Msg  
   resp.SetReply(req) // 创立应对  
   for _, question := range req.Question {  
      ip := cdnConfig[question.Name]  
      if len(ip) == 0 {  
         return  
      }  
      recordA := dns.A{  
         Hdr: dns.RR_Header{  
            Name:   question.Name,  
            Rrtype: dns.TypeA,  
            Class:  dns.ClassINET,  
            Ttl:    100,  
         },  
         A: net.ParseIP(ip).To4(), // 悉数解析为127.0.0.1  
      }  
      resp.Answer = append(resp.Answer, &recordA) // 写入应对  
   }  
   err := writer.WriteMsg(&resp) // 回写信息  
   if err != nil {  
      return  
   }  
}  
func main() {  
   dns.HandleFunc(".", handler)                   // 绑定函数  
   err := dns.ListenAndServe(":1053", "udp", nil) // 发动  
   if err != nil {  
      log.Println(err)  
   }  
}

测验

接着,咱们就能够测验下现在建立的这个cdn体系了,尽管咱们将许多装备信息都写死在了代码里,但你能够假装他们是自动装备上的。

来看下现在的代码结构.

cdn原理分析-本地搭建cdn模拟访问过程

cnddns便是模仿的cdn调度中心了,edgenode和sourceserver放的都是nginx的装备文件,docker-compose.yaml到时分会发动他们。

发动源站服务器,边际节点

(base) ➜  cdndemo git:(master) ✗ docker-compose up
Starting edgenode      ... done
Starting source-server ... done
Attaching to edgenode, source-server
source-server   | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
source-server   | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
source-server   | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh

发动cdn调度中心

(base) ➜  cdndemo git:(master) ✗ cd cdndns
(base) ➜  cdndns git:(master) ✗ go run main.go  

测验拜访

咱们现已把本地机器的dns服务器改为dnsmasq了,接着便是最终测验下拜访是否生效。

我在浏览器接连进行了两次拜访

http://web.cdn.test/static/1781686486866_.pic.jpg

接着查看下nginx的access.log

边际节点的日志

web.cdn.test- 172.22.0.1 - - [16/Jun/2023:08:53:12 +0000] "GET /static/1781686486866_.pic.jpg HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" "-"
web.cdn.test- 172.22.0.1 - - [16/Jun/2023:08:53:31 +0000] "GET /static/1781686486866_.pic.jpg HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" "-"

源站服务器日志

sourceserver- 172.22.0.3 - - [16/Jun/2023:08:53:12 +0000] "GET /static/1781686486866_.pic.jpg HTTP/1.0" 200 265084 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" "-"

能够看见当第二次拜访的时分,源站服务器上没有日志了,边际节点仍然有拜访日志,说明第二次拜访现已直接从边际节点读取到了图片信息。

仍是强烈建议把源码下下来亲自实验一遍,你会更加深入。