pacakge expvar 内部实现

内部数据结构

// Var is an abstract type for all exported variables.
type Var interface {
	// String returns a valid JSON value for the variable.
	// Types with String methods that do not return valid JSON
	// (such as time.Time) must not be used as a Var.
	String() string
}

// All published variables.
var (
	mutex   sync.RWMutex
	vars    = make(map[string]Var)
	varKeys []string // sorted
)

vars 的 key 是 Var 绑定的名称,Var 是 一个有 String() 方法的 interface。

func NewInt(name string) *Int {
	v := new(Int)
	Publish(name, v)
	return v
}

// Publish declares a named exported variable. This should be called from a
// package's init function when it creates its Vars. If the name is already
// registered then this will log.Panic.
func Publish(name string, v Var) {
	mutex.Lock()
	defer mutex.Unlock()
	if _, existing := vars[name]; existing {
		log.Panicln("Reuse of exported var name:", name)
	}
	vars[name] = v
	varKeys = append(varKeys, name)
	sort.Strings(varKeys)
}

NewInt 创建一个名字为 name 的 Int 对象,并将这个对象名字和对应对象的指针保存到 map vars, 并返回这个对象的指针 Int对象的指针有 Add()、Set()、String()、Value()等方法

// Int is a 64-bit integer variable that satisfies the Var interface.
type Int struct {
	i int64
}

func (v *Int) Value() int64 {
	return atomic.LoadInt64(&v.i)
}

func (v *Int) String() string {
	return strconv.FormatInt(atomic.LoadInt64(&v.i), 10)
}

func (v *Int) Add(delta int64) {
	atomic.AddInt64(&v.i, delta)
}

func (v *Int) Set(value int64) {
	atomic.StoreInt64(&v.i, value)
}

expvar 会向 http.DefaultServeMux 注册了 /debug/vars 的 handler 如果你的 Server 有自己的 handler ,那么 需要手动注册一下 /debug/var 的 handler

func init() {
	http.HandleFunc("/debug/vars", expvarHandler)
	Publish("cmdline", Func(cmdline))
	Publish("memstats", Func(memstats))
}

使用示例1: 不使用自己的hander

package  main

import (
 "expvar"
 "net/http"
 "fmt"
)

var visits = expvar.NewInt("visits")

func handleFunc (w http.ResponseWriter, r *http.Request) {
	visits.Add(1)
	fmt.Fprintf(w,"你是第%d 访客\n", visits.Value())
}

func main()  {
	http.HandleFunc("/", handleFunc)
	http.ListenAndServe(":8080", nil)
}

使用实例2: 使用自己的 handler

package main

import (
    "fmt"
    "github.com/julienschmidt/httprouter"
    "net/http"
    "log"
    "expvar"
)

var visited = expvar.NewInt("visited")

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	visited.Add(1)
    fmt.Fprint(w, "Welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	visited.Add(1)
    fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}

func Vars(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	expvar.Handler().ServeHTTP(w, r)
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)
    router.GET("/hello/:name", Hello)
    router.GET("/debug/vars", Vars)

    log.Fatal(http.ListenAndServe(":8080", router))
}

使用Context包

Context 简介

Context 是一个接口,它有四个方法:

  • Deadline() 当Context 是设置了超时时间的Context, Deadline 返回超时时间。如果没有设置超时时间,ok 为 false
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
  • Done() Done 返回的 Channel 会在以下情况被关闭
    1. 当 Context 的 Cancel 函数被调用后
    2. 当 Context 的 超时时间消耗完
    3. 当 Context 的 deadline 到期
    4. 该请求已完成,ServerHTTP()已经返回

从net/http/request.go:313 的注释中说明了Context 被取消掉的几种情况

// For incoming server requests, the context is canceled when the
// client's connection closes, the request is canceled (with HTTP/2),
// or when the ServeHTTP method returns.
  • Err() 当 Context 的 Cancel 函数被调用后, Err 返回 Canceled, 当超时时,返回 deadlineExceededError

  • Value() 当 key 存在时,返回对应 key 的 value, 否则返回 nil. 使用

    ctx := context.WithValue(context.Background(), k, "Go")
    

Context package 部分源码

var Canceled = errors.New("context canceled")

type deadlineExceededError struct{}

func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool   { return true }
func (deadlineExceededError) Temporary() bool { return true }

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

var (
	background = new(emptyCtx)
	to = new(emptyCtx)	
)

func BackGround() Context {
 	return background
}

type CacelFunc func()

// 返回一个子 Context 和 cancel 函数  
//当返回的 cancel 被调用时,Context.Done() 返回的 chan 将被关闭,它的子 context 的 cancel 也会被调用
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

作用

防止 goroutine 泄漏

// gen generates integers in a separate goroutine and
// sends them to the returned channel.
// The callers of gen need to cancel the context once
// they are done consuming generated integers not to leak
// the internal goroutine started by gen.
gen := func(ctx context.Context) <-chan int {
    dst := make(chan int)
    n := 1
    go func() {
        for {
            select {
            case <-ctx.Done():
                return // returning not to leak the goroutine
            case dst <- n:
                n++
            }
        }
    }()
    return dst
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers

for n := range gen(ctx) {
    fmt.Println(n)
    if n == 5 {
        break
    }
}

HTTP client 取消请求

// 创建一个可以取消的 Context
ctx, cancel := context.WithCancel(context.Background())

req, err := http.NewRequest("POST", "http://localhost", nil)

// 将 Context 保存到 新的 Request, 并返回它的指针,所以必须赋值给 req
req = req.WithContext(ctx)

// 执行请求
client := &http.Client{}
client.Do(req)

// 取消请求
cancel()

使用 Go 的并发特性来并行化一个 Web 爬虫

爬虫基本原理

爬虫程序从一个网页开始,获取它的内容之后,找到其中的URL,又继续抓取该URL的内容,继续找URL,再继续抓取它的内容。 需要考虑两个问题:

  • 一个URL 和另一个URL 可能形成环路,爬虫需要判断某个URL是否是第二次被抓取
  • 考虑效率,需要并行抓取URL

以下代码模拟了爬虫的基本过程,省略了从网页中解析出URL的过程

代码实现

package main

import (
	"fmt"
	"sync"
)

type SafeMap struct{
	v   map[string]bool
	sync.Mutex
}

var safemap = SafeMap{
	v:make(map[string]bool),
}

type Fetcher interface {
	// Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。
	Fetch(url string) (body string, urls []string, err error)
}

// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
// sync.WaitGroup 是结构体,必须传指针,否则会死锁
func Crawl(url string, depth int, fetcher Fetcher, waitgroup *sync.WaitGroup) {
	defer waitgroup.Done()
	if depth <= 0 {
		return
	}
	safemap.Lock()
	if isfetch := safemap.v[url]; isfetch {
		fmt.Println(url," is fetched")
		// 此处容易忘记解锁
		safemap.Unlock()
		return
	} else {
		safemap.v[url] = true
		safemap.Unlock()
	}
	body, urls, err := fetcher.Fetch(url)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("found: %s %q\n", url, body)
	wg := sync.WaitGroup{}
	for _, u := range urls {
		wg.Add(1)
		go Crawl(u, depth-1, fetcher, &wg)
	}
	wg.Wait()
	return
}

func main() {
	wg := sync.WaitGroup{}
	wg.Add(1)
	go Crawl("http://golang.org/", 4, fetcher, &wg)
	wg.Wait()
}

// fakeFetcher 是返回若干结果的 Fetcher。
type fakeFetcher map[string]*fakeResult

type fakeResult struct {
	body string
	urls []string
}

func (f fakeFetcher) Fetch(url string) (string, []string, error) {
	if res, ok := f[url]; ok {
		return res.body, res.urls, nil
	}
	return "", nil, fmt.Errorf("not found: %s", url)
}

// fetcher 是填充后的 fakeFetcher。
var fetcher = fakeFetcher{
	"http://golang.org/": &fakeResult{
		"The Go Programming Language",
		[]string{
			"http://golang.org/pkg/",
			"http://golang.org/cmd/",
		},
	},
	"http://golang.org/pkg/": &fakeResult{
		"Packages",
		[]string{
			"http://golang.org/",
			"http://golang.org/cmd/",
			"http://golang.org/pkg/fmt/",
			"http://golang.org/pkg/os/",
		},
	},
	"http://golang.org/pkg/fmt/": &fakeResult{
		"Package fmt",
		[]string{
			"http://golang.org/",
			"http://golang.org/pkg/",
		},
	},
	"http://golang.org/pkg/os/": &fakeResult{
		"Package os",
		[]string{
			"http://golang.org/",
			"http://golang.org/pkg/",
		},
	},
}

程序运行结果

found: http://golang.org/ "The Go Programming Language"
not found: http://golang.org/cmd/
found: http://golang.org/pkg/ "Packages"
http://golang.org/cmd/  is fetched
found: http://golang.org/pkg/fmt/ "Package fmt"
http://golang.org/  is fetched
http://golang.org/pkg/  is fetched
found: http://golang.org/pkg/os/ "Package os"
http://golang.org/pkg/  is fetched
http://golang.org/  is fetched
http://golang.org/  is fetched

企业级互联网分布式系统应用架构

一、互联网应用架构概览

架构需要解决以下痛点:

  • 跨运营商网络的一致性体验 (解决办法:BGP多线、CDN)
  • 支持突发的高流量访问并快速响应,并将数据存储 (解决办法:应用层横向扩展-负载均衡、应用层缓存、同步转异步处理、数据库读写分离)
  • 保证系统的高可用,减少故障和运维导致的不可用时间

用户访问网站的过程:

  1. 互联网缓存服务器 (缓存静态内容,并刷新静态内容)
  2. 负载均衡器 (将用户的访问分发到 Web Server)
  3. Web 服务器(将静态内容返回给缓存服务器或者用户,并将动态内容(如购物车、订单)发送给应用服务器)
  4. 应用服务器
  5. 数据库服务器
  6. 大数据平台(分析用户数据和行为)

二、提高网络访问速度

网络的构成:

  • 第一公里: 网站的服务器平台到运营商的网络. 网络访问速度取决于网络服务器的处理能力和网络的带宽的瓶颈

  • 中间: 中国电信、中国联通、中国移动、教育网. 网络运营商之间的互联瓶颈问题,不同地区骨干网之间的数据交换、传输、导致运输途中的路由阻塞和延迟

  • 最后一公里: 随着 4G 网络的推行,以及网络服务提供商的提速降价,已经得到极大的缓解

解决办法:

  • BGP多线

    BGP 技术就是通过技术手段,使得不同运营商之间能共同访问一个 IP,并且不同运营商之间都能达到最快的接入速度的网络技术。多线实际对应着多个运营商。

  • CDN (内容分发网络)

    目的是通过在现有的Internet 中增加一层新的网络架构,将网站的内容发到最接近用户的网络”边缘”,使用户能够就近取得所需的内容,解决 Internet 网络拥塞状况,提高用户访问网站的响应速度。 使用CDN后的 HTTP 请求处理流程如下图(图片来源于阿里云CDN介绍文档):

    使用CDN后的HTTP请求处理流程

三、提高应用响应速度

提高应用响应速度,就必须提高应用并发度,可以使用以下方法:
* 应用层横向扩展
* 使用应用层缓存
* 同步转异步
* 数据库读写分离及提高IO能力

应用层横向扩展

应用层横向扩展应用场景:

  • 每天中午,用户一起玩王者荣耀
  • 双11搞促销活动

现在工业界中,使用的负载均衡开源软件

应用层缓存

在应用服务器和数据库服务器中,加入缓存服务器,如memcached,redis 用在第一次访问热点数据时,以Key-Value 的形式保存到缓存服务器,并设置过期时间,第二次访问热点数据时先从缓存服务器中读取。 当热点数据被修改时,应该将缓存服务器中数据删除。缓存数据一般是更新频率低但读取频率高的数据。

同步转异步

当某个业务逻辑的处理时间较长(几百毫秒),就将这个业务逻辑拆分成同步逻辑和异步逻辑,从而减少应用的响应时间。 例如处理订单的时候,将支付作为同步逻辑,将扣减库存作为异步逻辑。将扣减库存逻辑封装成消息,放入消息队列中。异步处理模块会从消息队列中取出消息,再做相应的处理。这样不仅能减少应用的响应时间,也能降低数据库服务器的压力。

数据库读写分离

数据库的主要操作和相应的SQL:

  • 读:select
  • 写:insert、delete、update

例如库存表中,update 一条记录时,数据库会加锁,读操作的效率会降低。 应用服务器进行读操作时,从 只读库中读,进行写操作时,向 主库写。从而降低主库的压力,提升应用的处理速度。并且通过只读库能提高应用查询速度。但这种方式,只适应于能接收数据同步时有一定延时的应用。

SQL 简明教程

Structured Query Language

SQL 的全称是 Structured Query Language ,中文是结构化查询语言。 它以分号作为结束符。 以下以著名的开源关系型数据库MySQL 为例,介绍常见SQL语句。

关系型数据库

数据库是信息的集合, 可以把数据库分成一张张表, 当表与表之间有内在的联系时,这样的数据库就称为关系型数据库。 关系型数据库可以将数据库分成更小的单元,这样就更容易根据组织的需要来维护和优化它。

MySQL 数据库

下面将介绍开始使用 MySQL 的命令

  • 使用命令行工具行连接MySQL服务器程序
# -u 用户名 -p 密码 -h mysqld 的 IP 地址和 TCP 端口 
mysql -uroot -p passwd -h mysqlserverip:port
  • 使用命令行工具 mysqladmin 管理 MySQL 服务器程序 mysqld ,例如检查机器上 mysqld 是否正在运行。
mysqladmin ping
mysqladmin create databasename
mysqladmin drop databasename
  • 使用 mysql 创建数据库的命令如下:

    • 创建名称为database_name:
      create database database_name;
    
    • 显示MySQL 的已有的数据库
      show databases;
    
    • 切换到数据名为 company
      use company;
    

命令分类

以下是根据用途将 SQL 分成六大类

* 数据定义语句
	* create table
	* drop table
	* alter table
	* create view
	* drop view
	* create index
	* drop index
	* alter index

* 数据操作语句 
	* update
	* insert 
	* delete

* 数据查询语句
	* select

* 数据控制语句
	* revoke
	* grant
	* alter password
	* create synonym

* 数据监视语句
	* stop audit
	* start audit

* 事务控制语句
	* commit
	* savepoint
	* rollback
	* set transaction