zhengkang 2015-02-03T05:56:45+00:00 kejinlu@gmail.com CLR集成中配置文件的使用 2015-02-02T00:00:00+00:00 zhengkang /2015/02/CLR集成中配置文件的使用 之前提到了利用SQL Server的新特性CLR集成创建了一个存储过程,由于我这里的存储过程需要访问网络资源,一般的程序,对于这些服务器端的IP地址都是写入配置文件,那么对于CLR集成来说,它也是可以使用配置文件的。下面链接是我在StackOverFlow上面询问如何使用配置文件的链接,问题下面的回答完美解决了这个问题,点击这里,当然你也可以直接点击查看这篇文章

这里的配置文件比较特殊,不像是普通应用程序的APP.config,而是有一定的规则。首先,由于我们的C#程序代码是运行在SQL Server中的(CLR集成的方式),依赖于数据库进程,那么配置文件肯定也就是跟数据库进程有关系了,这里可以通过查看数据库的安装目录来找到数据库可执行程序的目录,也可以直接执行下面这段SQL脚本:

declare @SQLRoot varchar(1024)
exec master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE',
N'SOFTWARE\Microsoft\MSSQLServer\Setup', N'SQLPath', @SQLRoot OUTPUT
select @SQLRoot

进入\Binn这个目录,就会看见一个可执行程序sqlservr.exe,那么相应的,配置文件的名字也必须是sqlservr.exe.config,没有这个文件请自行创建,里面的内容可以和之前普通应用程序的配置文件相同,下面是我的配置文件的内容:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="connection" connectionString="127.0.0.1"/>
  </connectionStrings>
</configuration>

我这里比较简单,就配置了一个IP地址。在完成配置文件之后,程序代码里就能和平常使用配置文件一样获取配置值了,这里需要注意的是,配置文件的名字一定不能更改。要是还想获取更细节的东西可以去看上面给出的那篇文章。

下面说一点这个昨天遇到的一个问题,在CLR集成里面,代码中完成了一个获取CDC表数据,并且连接TCP Server程序,将CDC表数据发送给TCP Server的功能,刚开始是获取一个值,就向服务端进行发送,由于表中数据量很大,所以发送数据这个过程非常的耗时,每次都对socket进行写入操作。后来是使用MemoryStream这个类,将所有从表中读取的数据,全都先放到这个内存流里面,之后将这个流里面的数据一次性发送到服务端,这样大大减少了发送时间。

以上所写还未进行实际线上验证,特别是在存储过程里面获取CDC表数据并通过TCP发送到服务端,之后几天会根据这个思路完成一个比较完整的更新模块。

补充:总共两种方式获取数据,一种就是上面提到的,直接在存储过程里把表数据全部发送出来,另一种就是,存储过程对这些表做轮询,当发现有表数据更新时通知服务端,服务端主动获取更新数据,当然了,这个通知也还是通过存储过程发送出来的(与服务端TCP连接)。

]]>
SQL Server数据库数据与内存中存储数据的同步问题 2015-01-27T00:00:00+00:00 zhengkang /2015/01/SQL-Server数据库数据与内存中存储数据的同步问题

这段时间在重新实现DataCenter与数据库数据的更新的模块,由于使用的数据库为SQL Server,所以就查阅了一些数据库本身对更新的支持,下面就说说整个查找过程吧。

CDC及Change Tracing

由于使用的sql server为2008版,所以就首先去了解了CDC(Change Data Capture)这个功能,它能通过对日志文件的读取获得数据表经过了哪些数据更新(DML及DDL)操作,并且记录更新操作是一个异步的过程,关于如何使用及一些注意事项网上资源也挺多的,不过还是要提醒一点,在启用的时候千万别忘记启动数据库代理功能,不然CDC就无法工作。Change Tracing和CDC差不多,可以当作一个轻量版本的CDC吧,但是他是同步的,在一个事务中完成,所以在这里也被PASS了。

Service Broker

当时在stackoverflow上也咨询过这方面的问题,当时是看到它有一个队列,可以将更改信息发送到队列保存,使得应用程序能够通过队列获取到数据表的数据更新信息,从而只需要监听这个队列就能及时的获取变更数据,不过这种发送数据到队列的操作是需要由数据库在一个触发器中主动执行SEND操作,但是在我们当前的环境中是不允许使用触发器,所以作罢。后来看到stackoverflow上的一个老哥介绍说试试QueryNotification,并且还很热情的给出了一部分使用代码,在查询了这个功能及与他做了一些沟通之后,因为它会一直占用一个数据库连接并且不能给出相对有用的更新信息及业务上的一些其它原因,PASS。

CLR集成

其实在当时在测试Service Broker的时候,是想利用CDC+Service Broker来完成这个数据的功能,但是上面也说了由于Service Broker会一直占用一个连接,所以就没使用,后来是想着使用CLR集成做一个存储过程,将这个存储过程放到一个数据库JOB中,JOB完成轮询CDC表的过程,当发现有数据更新时就执行存储过程,而这个存储过程就完成一个功能,将CDC表中的数据发送到外界的应用程序。个人感觉CLR集成是SQL Server中比较好的一个功能,能让数据库调用C#编写的类库,极大的方便了存储过程功能的实现。这里要注意一个问题,就是因为要通过网络把数据从数据库中发送出来,所以在创建程序集的时候需要开启UNSAFE权限。

例如:CREATE ASSEMBLY TcpClr FROM 'G:\TcpClrTest.dll' WITH PERMISSION_SET = UNSAFE

当然有时候执行这条语句的时候会失败,给出的出错信息也比较详细,根据出错信息修改问题应该不大,大概就是说不是数据库所有者(DBO)及需要什么证书之类的。在查阅了MSDN之后,解决方法也比较容易

ALTER DATABASE DBNAME SET TRUSTWORTHY ON

上面这条语句相对比较简单,还有一种方法就是创建非对称证书,具体可以查询MSDN,根据自己的业务环境选择一个方法。

GRANT EXTERNAL ACCESS ASSEMBLY TO LOGINNAME

目前的一个方案是:有一个服务端在ACCEPT,等待数据库端连接;数据库端有一个作业每隔一段时间都会轮询一遍所有CDC的表,若发现有数据更新,则连接服务端,将更新数据及相关的操作信息发送给服务端,服务端根据这些信息做出数据的更新。

当然这个只是一个初步的想法,在逻辑上是可行的,具体还是要和业务相结合并做出相应的修改。以上说的比较简单与笼统,其中还有很多细节需要注意,相应的知识点也可以很方便的在google搜寻到,还有就是千万别忽略了stackoverflow,在上面查询你的问题,大部分都是能直接检索出你想要的答案,若没有,也可以发起一个提问,过不了多久就会有人帮你解答。

]]>
关于select中的一个case-channel的阻塞问题 2015-01-16T00:00:00+00:00 zhengkang /2015/01/关于select中的一个case-channel的阻塞问题

昨天在群里看到有人发了一段代码,是个关于select中case的阻塞问题,第一次见到,所以就自己运行后写点到目前为止的想法,不一定正确。

Code

package main

    import (
        "fmt"
        "time"
    )
    
    func main() {
        fmt.Println("start")
        c := make(chan struct{})
        //c := make(chan struct{}, 1)
        go slet(c)
        time.Sleep(time.Second * 3)
        c <- struct{}{}
        time.Sleep(time.Second * 3)
    }
    
    func slet(ch chan struct{}) {
        for {
            select {
            case ch <- <-ch:
                fmt.Println(len(ch))
            default:
                fmt.Println("default")
            }
        }
    }

代码部分稍微做了一下小修改,这段代码的运行结果是只打印了一次"default"。

上面比较有意思的在select中的ch<-<-ch,说实话我是第一次见到go程序中出现这个东西,也许这和我go程序写的少也有关系吧。总之在看到代码的时候我还是没想到只打印一次"default",既然有了结果,那就根据结果自己做一个猜想。打印出的信息也是在3秒之后才开始打印出来的,所以当goroutine运行的时候,在main还在暂停3秒的时候一直处于阻塞的状态,这个阻塞就发生在select块中的ch<-<-ch这部分。这就很奇怪了,不是说select语句中当有某个case发生阻塞的时候会继续寻找下面的不发生阻塞的case吗,况且这里还有一个default存在,按理说就算所有的case发生阻塞那么也是会执行default的,而这里偏偏没有执行。

这里要注意到我们之前定义的channel类型的变量c是没有缓存的,意思就是说当执行c<-value的时候,要是没有<-c读取操作,那么c<-value是会一直阻塞着的,直到有读取操作的发生。那么当运行slet的时候,很显然main还在休眠并没有执行到c<-struct{}{},也就是说ch<-<-ch这个操作就一直阻塞在后面一个<-ch中,也就是说这整个case语句压根就没有判断结果的出炉。

那么这里也出现了阻塞,为什么没有运行下面的default,我的理解是,select中说的case阻塞,那么久会运行default操作,这个阻塞是当case后面的操作完成后判断出是阻塞的,那么才会运行default操作,要是这个操作一直没有完成,那么它会等待这个操作完成来判断是否是阻塞的。而上面的代码中,由于第二个<-ch一直处于等待状态,导致这个整个操作也一直处于等待的状态。换句话说,第二个<-ch操作可以理解为一个死循环,当有一个break条件让它跳出的之后,它才会运行结束,而这个地方的break条件就是main中的c<-struct{}{}

可能ch<-<-ch看起来不是很清楚,我们可以把后面一个<-ch看成是一个函数

func dowhile(ch chan struct{}) struct{} {
        return <-ch
    }

那么ch<-<-ch就变成了ch<-dowhile(ch),这下问题可能就清晰多了。

我个人觉得,对上面的问题会有不同答案的原因还是对于阻塞的理解。当select中所有的case阻塞的时候就会执行select块中的default语句,对这个阻塞的含义的理解是比较重要的。

上面的就是我自己的一些个人理解,具体的select操作时如何实现的还是要看源代码来理解(我还未看过代码是如何实现的),要是理解错误或者偏了还望大神及时指出。

]]>
海南来上海 2014-12-30T00:00:00+00:00 zhengkang /2014/12/海南来上海

去年七月大家各奔东西,至今一年半有余,宿舍五个人,一个在南京,一个在深圳,两个在西安,而我在上海,工作的这段时间也偶尔会在宿舍群里吹吹牛逼,开开玩笑,还是像大学里过去的四年一样,一样的欢乐。这一年,舍长大人也完成了他人生中的一件大事---终于结束了和他女朋友的长跑,当年那个在军训完之后喘着粗气,奔进宿舍大声嚷嚷要让所有的朋友帮忙帮他挽回女朋友的舍长如今也已经得偿所愿,骗得美人归。

当前段日子听海南说要骑自行车从西安回海南岛,我们还是暗暗的为他的屁股担心。但就在前天中午收到他的消息说可能晚上6点左右会到达上海,我还在想这家伙有这么快速度,这单身了这么多年不仅把手速练上去了,难道脚速也跟着上去了,在问了情况后知道是从南京火车过来,自行车只是在骑到河南的信阳屁股就已经伤不起了,又因为想在元旦的时候到家所以就直接让火车带节奏了。查了车站后就把地铁路线发给了他,但由于带着自行车,最后还是轮渡黄浦江,骑过来了。

快十二点了去路口接到了他,两人挤在一张床上(他自备睡袋),虽然第二天还是要上班,但还是聊到了很晚,从他路上的见闻聊到生活,从生活又聊到游戏,最后谁也不知道谁先睡着的。昨晚下班后又带着他逛了外滩和南京路,其实一路上周围的环境倒是没怎么观赏与介绍评论,倒是一直说着我们这一年多来遇到的事情,也回忆着学校的生活,从大一就开始嚷着要一起去的华山终究到了毕业也还没有落实,消失了的6蛋蛋,被隔离的那几天,还有那些年缠绕在身上的绯闻。

晚饭这家伙狂点一大桌,两个人吃的倒也没有剩下多少,吃撑了的我们实在不想动,就坐着,吹牛逼,说了当年刚进学校的时候还是我带着他办理了所有的入学手续,带他买了生活必需品,还吐槽他这么远的距离还带着个水壶来西安.....

今天早上送他,自行车装的满满的,行程没有继续,直接抗着自行车上了火车,奔回海南了。另外昨晚你真的是踹到我的头了。

ps:他骑的这辆自行车在去年是沿着川藏线入藏的,今年又将会跟着他去海南。

]]>
groupcache如何使用的一个简单例子 2014-12-03T00:00:00+00:00 zhengkang /2014/12/groupcache如何使用的一个简单例子

在网上查了挺多groupcache的相关信息,但是搜出来大部分都是copy,实际的例子也没有,所以就看了下源码,也在golang-nuts上问了,github上搜索了一些groupcache的使用例子,在此作个总结,目前对这个缓存库还仅处于了解状态

大概介绍

其实关于groupcache的介绍网上非常的多,搜索出来清一色都是说的介绍,当然也有配图如何部署,但是文字与配图完全不在一个时空,图也是copy国外的一篇博客。它不像其它的一些缓存数据库有个服务端,需要客户端去连接,文档中明确说明了它就是一个程序库,所以没有服务端与客户端这么一说,换句话说,它本没有服务端,又或者是人人都是服务端,食神的感觉油然而生。

主要代码结构

它的代码结构也比较清晰,代码量也不是很大,很适合大家去阅读学习。主要分为了consistenthash(提供一致性哈希算法的支持)lru(提供了LRU方式清楚缓存的算法)singleflight(保证了多次相同请求只去获取值一次,减少了资源消耗),还有一些源文件:byteview.go 提供类似于一个数据的容器http.go提供不同地址间的缓存的沟通的实现peers.go节点的定义sinks.go感觉就是一个开辟空间给容器,并和容器交互的一个中间人groupcache.go整个源码里的大当家,其它人都是为它服务的

一个测试例子

在使用这个缓存的时候比较重要的几方面也是我之前犯错的几个地方

  • 需要监听两个地方,一个是监听节点,一个是监听请求
  • 在批量设置节点地址的时候需要在地址前面加上http://,因为一开始我没有加上去,所以缓存信息一直不能再节点之间交互
  • 启动的节点地址要与设置的节点地址一致:数量和地址值。因为我每次少一个就无法在节点间交互。

以上的一些信息可能也有不对的地方,只是我个人的一个测试后的结果。

代码部分

package main

        import (
            "flag"
            "fmt"
            "github.com/golang/groupcache"
            "io/ioutil"
            "log"
            "net/http"
            "os"
            "strconv"
            "strings"
        )
        
        var (
            // peers_addrs = []string{"127.0.0.1:8001", "127.0.0.1:8002", "127.0.0.1:8003"}
            //rpc_addrs = []string{"127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"}
            index = flag.Int("index", 0, "peer index")
        )
        
        func main() {
            flag.Parse()
            peers_addrs := make([]string, 3)
            rpc_addrs := make([]string, 3)
            if len(os.Args) > 0 {
                for i := 1; i < 4; i++ {
                    peers_addrs[i-1] = os.Args[i]
                    rpcaddr := strings.Split(os.Args[i], ":")[1]
                    port, _ := strconv.Atoi(rpcaddr)
                    rpc_addrs[i-1] = ":" + strconv.Itoa(port+1000)
                }
            }
            if *index < 0 || *index >= len(peers_addrs) {
                fmt.Printf("peer_index %d not invalid\n", *index)
                os.Exit(1)
            }
            peers := groupcache.NewHTTPPool(addrToURL(peers_addrs[*index]))
            var stringcache = groupcache.NewGroup("SlowDBCache", 64<<20, groupcache.GetterFunc(
                func(ctx groupcache.Context, key string, dest groupcache.Sink) error {
                    result, err := ioutil.ReadFile(key)
                    if err != nil {
                        log.Fatal(err)
                        return err
                    }
                    fmt.Printf("asking for %s from dbserver\n", key)
                    dest.SetBytes([]byte(result))
                    return nil
                }))
        
            peers.Set(addrsToURLs(peers_addrs)...)
        
            http.HandleFunc("/zk", func(rw http.ResponseWriter, r *http.Request) {
                log.Println(r.URL.Query().Get("key"))
                var data []byte
                k := r.URL.Query().Get("key")
                fmt.Printf("cli asked for %s from groupcache\n", k)
                stringcache.Get(nil, k, groupcache.AllocatingByteSliceSink(&data))
                rw.Write([]byte(data))
            })
            go http.ListenAndServe(rpc_addrs[*index], nil)
            rpcaddr := strings.Split(os.Args[1], ":")[1]
            log.Fatal(http.ListenAndServe(":"+rpcaddr, peers))
        }
        
        func addrToURL(addr string) string {
            return "http://" + addr
        }
        
        func addrsToURLs(addrs []string) []string {
            result := make([]string, 0)
            for _, addr := range addrs {
                result = append(result, addrToURL(addr))
            }
            return result
        }

执行方式:./demo 127.0.0.1:8001 127.0.0.1:8002 127.0.0.1:8003

在上面的命令中我们就启动了一个节点,并且设置节点地址个数为3个,这里由于我是默认的index为0,所以在启动其它节点的时候变换下地址的顺序,使第一个地址三次都不一样就好了。(抱歉,这里实现的不是很好),这样同样方法启动三个节点。

打开浏览器访问127.0.0.1:9001?key=1.txt,这里1.txt是需要获取数据的之际地方,类似于实际中的数据库,我这里直接用一个文件代替了。

运行结果:

  • 当我访问:9001时结果

在上面图中,我们像:8001这个节点请求数据,它没有缓存数据,然后会去其它节点中寻找这个key的数据,当都没有的时候就会去数据库中获取(这里我们是一个文件),所以会出现在:8003这个节点中获取数据库数据的操作。

  • 然后我访问:9002

根据上图看到,第一个地址为:8002这个节点直接从缓存里面取值,但是在请求之前这个节点并没有缓存数据,这个也同样是节点间的交互完成的。

我在本地开启了两个虚拟机,在同一个局域网中,测试也能得出相同的结果。这里结果就不再贴上了,当然运行的时候节点地址要做相应的变动,根据每个机子的局域网中的地址。

这篇博客关于groupcache的介绍和源代码的说明部分比较少,主要就是贴出了一个测试的例子,因为我看到网上很少,并且压根就没有给出如何运行或者运行结果(不包括刚才提到的老外的一个博客,他写的还是很好的,我就是看了他的博客才着手自己编写的)。

]]>
一致性Hash算法浅析 2014-11-26T00:00:00+00:00 zhengkang /2014/11/一致性Hash算法浅析

上一篇博客里的Dynomite中采用了一个一致性哈希环,所有就去看了一致性哈希算法,下面的内容为对这个算法的一个个人总结

概念

在分布式缓存当中,一致性哈希算法会经常出现,用来解决如何在分布式网络中分布存储和路由这个问题。因为在分布式的环境中存在很多的节点,数据存储在每个节点上,一般的哈希算法在节点变化的过程中会对所有的数据存放都进行重新分配,这将会在资源的耗费上消耗很多。而一致性哈希算法在节点的变化中只会影响到相应节点的数据存储,而不是全部数据,这点就让人非常的开心。

算法描述

现在假设我们所有的缓存节点都是分布在一个环上,这些节点都是根据相应的hash函数计算出在环中的位置的。现在有一些数据需要存储在这些缓存节点当中,那么这些数据是如何选择自己所要的待着的节点呢,也是通过hash函数计算。很明显根据hash函数,节点的计算结果正常来说都不会相同,当然了相同的话也没关系。要是相同的话就直接存储到这个节点上,不同那么就顺时针选择一个离自己最近的一个节点。

上图所示,这个环上有三个节点,根据hash函数计算出了他们在环上所处的位置,另外存储了七个数据,这七个数据也根据hash函数计算出了他们的位置,那么根据规则:1、2被存储到了Node1节点;3、4被存储到了Node2节点;5、6、7被存储到了Node3节点。这部分的功能其实一般的hash算法都能做到。

下面就看看要是我们这时假设有一个节点故障或者移除了会出现什么情况

在上图中,我们移除了节点2(Node2),那么它本身因为有数据存储在其中,这里为数据3、4,所有这两个数据需要被转移到别的节点上,根据规则,顺时针寻找离自己最近的节点,它们找到了节点3(Node3),那么数据就存储到Node3上,而其它的数据我们看到存储完全没有变化,也就是说移除节点只跟这个节点上存储的数据有关。同样的,当增加节点时,影响的是从新的节点开始逆时针往上找到的第一个节点之间的数据。

那么下面问题来了,当我们在移除Node2后,3、4的数据全部都存储到了Node3上,而Node1只存储了数据1、2。那么就会出现不平衡的状态,有些节点存储的数据非常多,有些节点又非常的少,一致性hash算法采用了虚拟节点来解决这个问题。也就是说一个实际的物理节点,会复制出若干个虚拟的物理节点,这些虚拟节点的位置和物理节点不相同,也是根据hash函数计算后分布在这个环上的,但是虚拟毕竟是虚拟,所有的数据实际上是存储在物理节点上的。

上图中,Node1-2为物理节点Node1-1的虚拟节点;Node3-2为Node3-1的虚拟节点,这样就会有效的防止某个物理节点上存储的数据过多,而某些节点存储数据过少这种情况。

上图中,我们有实际节点1、2;根据实际节点虚拟出来的虚拟节点1-1、2-1、1-2、2-2;需要存储的数据1、2、3、4、5、6;

假设现在数据1、5是存储在节点2-1中;数据2、6是存储在1-2中;数据3存储在1-1;数据4存储在2-2;所以实际数据存储情况为,数据2、3、6存储在物理节点1中,1、4、5存储在物理节点2中。每个物理节点虚拟出来的虚拟节点个数可以根据实际情况来选择。

代码

下面是取自groupcache中实现的一致性hash代码中,我做了部分修改。

package main
        
        import (
            "fmt"
            "hash/crc32"
            "sort"
            "strconv"
        )
        
        type Hash func(data []byte) uint32
        
        type Map struct {
            hash     Hash
            replicas int
            keys     []int // Sorted
            hashMap  map[int]string
        }
        
        func New(replicas int, fn Hash) *Map {
            m := &Map{
                replicas: replicas,
                hash:     fn,
                hashMap:  make(map[int]string),
            }
            if m.hash == nil {
                m.hash = crc32.ChecksumIEEE
            }
            return m
        }
        
        // Returns true if there are no items available.
        func (m *Map) IsEmpty() bool {
            return len(m.keys) == 0
        }
        
        // Adds some keys to the hash.
        func (m *Map) Add(keys ...string) {
            for _, key := range keys {
                for i := 0; i < m.replicas; i++ {
                    hash := int(m.hash([]byte(strconv.Itoa(i) + key)))
                    m.keys = append(m.keys, hash)
                    m.hashMap[hash] = key
                }
            }
            sort.Ints(m.keys)
        }
        
        // Gets the closest item in the hash to the provided key.
        func (m *Map) Get(key string) string {
            if m.IsEmpty() {
                return ""
            }
        
            hash := int(m.hash([]byte(key)))
        
            // Binary search for appropriate replica.
            idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] >= hash })
        
            // Means we have cycled back to the first replica.
            if idx == len(m.keys) {
                idx = 0
            }
        
            return m.hashMap[m.keys[idx]]
        }
        
        func (m *Map) Del(key string) {
            if m.IsEmpty() {
                return
            }
            var hash uint32
            for i := 0; i < m.replicas; i++ {
                //log.Fatal("not panic:", i)
        
                hash = m.hash([]byte(strconv.Itoa(i) + key))
                // fmt.Println(m.hashMap)
                delete(m.hashMap, int(hash))
                // fmt.Println(m.hashMap)
                // fmt.Println(m.keys)
                ids := sort.Search(len(m.keys), func(i int) bool {
                    //fmt.Printf("m.keys[%d]:%d-->hash:%d\n", i, m.keys[i], int(hash))
                    return m.keys[i] >= int(hash)
                })
                copy(m.keys[ids:], m.keys[ids+1:])
                m.keys = m.keys[:len(m.keys)-1]
        
            }
        }
        
        func main() {
            ring := New(3, nil)
            ring.Add("zkkk", "fanp", "lixm", "ppoo", "weir", "zhgk")
            for i := 0; i < 20; i++ {
                str := fmt.Sprintf("James.km%03d", i)
                name := ring.Get(str)
                fmt.Printf("[%16s] is in node: [%16s]\n", str, name)
            }
            ring.Add("zouq")
            fmt.Println("after add new node")
            for i := 0; i < 20; i++ {
                str := fmt.Sprintf("James.km%03d", i)
                name := ring.Get(str)
                fmt.Printf("[%16s] is in node: [%16s]\n", str, name)
            }
            ring.Del("zouq")
            fmt.Println("after delete zouq node")
            for i := 0; i < 20; i++ {
                str := fmt.Sprintf("James.km%03d", i)
                name := ring.Get(str)
                fmt.Printf("[%16s] is in node: [%16s]\n", str, name)
            }
        }
]]>
Dynomite-让非分布的数据存储变成分布式的 2014-11-20T00:00:00+00:00 zhengkang /2014/11/Dynomite-让非分布的数据存储变成分布式的

一篇讲关于Dynomite的博客文章,看了觉得不错,就自己根据原文做了部分翻译,很多地方可能翻译的不是很恰当,语句通顺上面也还是需要修饰。(点击原文)

Dynomite-让非分布的数据存储变成分布式的

服务架构

目的

在开源的世界里有很多单服务器的数据存储解决方案,例如: Memcached, Redis, BerkeleyDb, LevelDb, Mysql (datastore)。这些解决方案通常采用master-slave配置。当请求非常的多,需要处理大量的流量的时候,我们就会引入集群机制。大多数人都同意,这将非常的不简单。另外,对开发者来说,这在管理数据上面也是一个挺大的挑战。

Dynomite拓扑结构

一个Dynomite集群包括了多个数据中心(dc)。每个数据中心都有多个机架(racks),每个机架包含了多个节点。每个机架又包含了完整的数据集,这些数据集被分配在机架的多个数据节点上。所以,多个机架就能够提供数据服务的高可用性。在机架上,每一个节点都拥有一个特殊的令牌(token)号,这个令牌标记用来识别出每一个节点所拥有的数据集。

每个Dynomite节点(例如:a1,b1,c1)都有一个Dynomite进程(Process)来协调提供数据服务,他扮演了一个类似于代理(proxy),路由(traffic router)或协调者(coordinator)的角色。在 Dynamo paper中,Dynomite是位于Dynamo层,并且提供了额外的可插拔数据存储代理的支持来尽可能的维持本地数据存储协议。

数据存储方式可以像Memcached 或 Redis这样的内存数据库,也可以像Mysql, BerkeleyDb 或 LevelDb这样的数据库,当前Dynomite支持Redis和Memcached。

数据复制

客户端能够连接到机架上的任何一个节点来发送写数据请求,如何这个节点恰好就是写请求数据的拥有着,那么该数据就会首先会被写到本地的数据服务中,并且会异步的复制到所有数据中心集群中的机架中。如果这个节点不是数据的拥有着,则它会做出协调将数据发送给同一个机架中的拥有者节点,然后同样的在所有的数据中心做一个异步的复制操作。

在当前的实现中,当一个节点在本地机架中成功的执行了数据请求操作并且异步复制到其它远程数据中心的时候,协调者会返回给客户端一个OK标记。

下面的一张图展示了一个客户端将一个写数据请求发送给了一个不是数据拥有者的例子。该数据属于a2,b2,c2和d2节点,请求被发送到了a1,然后a1将会充当一个协调者将请求转发到合适的节点。

高可用读请求

多数据中心及多机架模型提供了高可用服务。客户端能够连接到任意一个节点来读数据。跟写操作一样,当这个节点是读数据请求的数据拥有着的时候,那么它就直接提供读服务。否则,它就会将这个读请求发送到同一个机架的请求数据拥有着节点。当在远程数据中心或者机架进行数据复制操作失败的时候,Dynomite客户端将会请求失败。

可插拔的数据存储

Dynomite当前支持Redis和Memcached,这里感谢TwitterOSS Twemproxy这个项目。根据我们的经验,这两个数据库的最常用的API接口我们的已经支持。其它额外的API接口我们会在不久的将来支持。

标准的Memcached/Redis协议支持

任何可以跟Memcached或Redis进行数据交互的客户端也都可以跟Dynomite进行交互,你不需要改变什么。然而,有部分方式是不支持的,例如:故障转移策略,请求限制,连接池等。(更多的细节在客户端架构一节中)

IO事件通知服务的可扩展性

所有的输入输出流都在单线程的IO事件循环中被执行,其它额外的线程是执行后台或者管理的任务。所有的线程是通过无锁环队列来发送消息进行沟通的,消息是异步执行发送。这个实现机制能够让每一个Dynomite节点都能在处理大量客户端连接的同时仍然能够执行许多非客户端的并行任务。

Peer to peer 和线性可扩展性

每个Dynomite集群中的节点都有相同的职责,所以不存在单点故障。有了这个优点,我们就可以添加更多的节点到Dynomite集群中来。

冷热缓存

现在,这个功能在对Redis做数据库的时候是有用的,Dynomite能够帮助填满peers中的空节点从而减少性能损耗。

非对称多数据中心复制

上面提到,一个写数据操作中,数据将会被复制到多个数据中心,在不同的数据中心,Dynomite可以被配置成拥有不同的机架和节点。当在不同数据中心请求流不平衡的时候将会有极大的帮助。

节点间的沟通和gossip

Dynomite内置了gossip来帮助维护集群成员以及故障检测和恢复。这简化了Dynomite集群的维护。

]]>
用go语言实现的一个简单的treap 2014-11-17T00:00:00+00:00 zhengkang /2014/11/用go语言实现的一个简单的treap Treap的简单介绍

顾名思义,Treap=Tree+Heap。这里引用NOCOW里的一句话描述:Treap,就是有另一个随机数满足堆的性质的二叉搜索树,其结构相当于以随机顺序插入的二叉搜索树。其基本操作的期望复杂度为O(log n)。 其特点是实现简单,效率高于伸展树并且支持大部分基本功能,性价比很高。更加详细的描述可以去维基百科上查阅。 没错,它实现起来相对于红黑树或者AVL树来说确实没那么的复杂,下面就实现一个go语言的版本。

代码部分

由于Treap=Tree+Heap,所以树和堆的特点,它兼而有之。根据上面的定义,我们需要一个随机数来满足堆的性质,下面看看节点的定义代码

type Node struct {
        data       int
        priority   int64
        leftChild  *Node
        rightChild *Node
    }

结构体中的priority就是一个存储优先级的变量。有了节点的结构定义,下面就要创建这个节点

/*创建节点*/
    func NewBSTNode(x int) (node *Node) {
        node = &Node{
            data:       x,
            priority:   rand.Int63(),
            leftChild:  nil,
            rightChild: nil,
        }
        return
    }

由于它的值是一个随机数,所以我们这里采用rand.Int63()来生成它。它除了优先级之外还有一个存储数据的变量data,还有一个左子树leftChild跟右子树rightChild,除了这个优先级的变量定义之外和一般的二叉树定义没有什么区别。为了理解起来方便,在代码中又定义了一个创建树的函数

/*创建一课BST树*/
    func NewBST() *Node {
        return nil
    }

函数里面什么也没做,只是简单的返回了一个nil

有了树有了节点,那就要把节点插入到树中

func Insert(t, node *Node) *Node {
        if t == nil {
            t = node
        } else if t.data > node.data {
            t.leftChild = Insert(t.leftChild, node)
            if t.priority > node.priority {
                t = rightRotato(t)
            }
        } else {
            t.rightChild = Insert(t.rightChild, node)
            if t.priority > node.priority {
                t = leftRotato(t)
            }
        }
        return t
    }
  • 首先判断这棵树是否为nil,若是则直接把node赋给t,返回。
  • 若不为nil则判断待插入的节点的数据域

    若D.data>A.data,则D插入到左子树,否则插入右子树。这个插入操作是递归实现的。 到目前为止,所有的操作跟一般的二叉查找树没有区别。接下去Treap还会比较优先级,为了维护堆性质,根据优先级修改之前插入好的二叉查找树,修改是通过旋转来实现的。有左旋转和右旋转。

先看左旋转代码

/*左旋转*/
    func leftRotato(p *Node) *Node {
        b := p.rightChild
        p.rightChild = b.leftChild
        b.leftChild = p
        return b
    }

p为当前子树的根节点,旋转的前提是,插入的子节点的priority比根节点的小,不符合堆性质,所以旋转,旋转的目前是让p和子节点根据规定交换位置,这个规定就是不能破坏二叉查找树的性质。 代码中,首先保存了p的右子树到b,b就是一个子节点。由于这个旋转是由下自上进行的,所以在进行到b的时候,以它为根节点的树是符合性质的。因为b的左子树中节点的data肯定比p的data大,所以将b的左子树赋给p的右子树(p.rightChild = b.leftChild),然后把p赋给b的左子树(b.leftChild = p),现在b就是这棵子树的根节点,一次左旋转完成。

右旋转代码

/*右旋转*/
    func rightRotato(p *Node) *Node {
        b := p.leftChild
        p.leftChild = b.rightChild
        b.rightChild = p
        return b
    }

右旋转的方式跟左旋转一样,只是换了一个方向而已。

这里关键点为,当我们插入一个新节点的时候,在未旋转之前,这个节点肯定是插入到页节点上,并成为一个新的页节点,在插入之后判断是否需要旋转,若需要旋转,这个旋转肯定是从刚插入的父节点为根节点的子树开始做旋转操作的。

遍历

在建立了一棵treap树之后,查找数据就需要用到遍历的方法,遍历分为中序,前序,后序。

//中序遍历
    func (node *Node) midPrint() {
        if node != nil {
            node.leftChild.midPrint()
            fmt.Println(node.data, node.priority)
            node.rightChild.midPrint()
        }
    }

前序和后序的方法差不多,只是打印节点值的位置不一样。

可能有些地方描述的还不是很清楚。关键是这个旋转,这里的旋转比红黑树的旋转要简单的多。

]]>
配置vim+golang开发环境 2014-11-13T00:00:00+00:00 zhengkang /2014/11/配置vim+golang的开发环境

一直在使用Subline Text 2写go代码,但是一直想配置个vim+golang的开发环境,没有VIM这个环境就是很不舒服,网上看了挺多的方式,但是感觉每个博客或者回答都是雷同的,大部分都是直接A copy B。总感觉他们都没有自己真正的实践过方法是否可行,或者试过了但是没有将解决问题的关键点写出来,比如在配置的过程中会遇到什么问题等。

环境需求

要配置使用vim写golang代码,首先机子上要有golang的环境,这部分具体怎么搞网上非常的多,按着教程都不会有问题,我在这里就说说自己在配置vim开发的时候遇到的一些问题。我的机子上的系统是ubuntu 14.04,go 版本为1.3。

安装插件

vim的配置都知道需要安装某些插件还有写配置文件

插件管理

关于vim的插件管理的工具网上看到就两种比较多(不知道有没有其它比较好用的),Pathogen和Vundle,这两个插件我都用过,以前是用Pathogen,现在是用Vundle,这个就看哪个比较习惯了。关于这两种插件的用法网上的资料也非常的多,这里也就不罗嗦了。我这里使用的是Vundle这个插件,并且也是看这个博客配置的。

插件

有了插件管理工具,那就可以安装插件了。我是首先直接把vimrc的配置文件写好的,这里我就贴出我的配置文件吧

1 set cursorline
2 set encoding=utf-8
3 set number
4 set nocompatible              " be iMproved, required
5 filetype off                  " required
6 "colorscheme molokai
7 
8 " set the runtime path to include Vundle and initialize
9 set rtp+=~/.vim/bundle/Vundle.vim
10 call vundle#begin()
11 
12 " let Vundle manage Vundle, required
13 Plugin 'gmarik/Vundle.vim'
14 Plugin 'fatih/vim-go'
15 Plugin 'Valloric/YouCompleteMe'
16 Plugin 'Lokaltog/vim-powerline'
17 Plugin 'SirVer/ultisnips'
18 
19 " All of your Plugins must be added before the following line
20 call vundle#end()            " required
21 filetype plugin indent on    " required
22 
23 " set mapleader
24 let mapleader = ","
25 
26 " vim-go custom mappings
27 au FileType go nmap <Leader>s <Plug>(go-implements)
28 au FileType go nmap <Leader>i <Plug>(go-info)
29 au FileType go nmap <Leader>gd <Plug>(go-doc)
30 au FileType go nmap <Leader>gv <Plug>(go-doc-vertical)
31 au FileType go nmap <leader>r <Plug>(go-run)
32 au FileType go nmap <leader>b <Plug>(go-build)
33 au FileType go nmap <leader>t <Plug>(go-test)                     
34 au FileType go nmap <leader>c <Plug>(go-coverage)
35 au FileType go nmap <Leader>ds <Plug>(go-def-split)
36 au FileType go nmap <Leader>dv <Plug>(go-def-vertical)
37 au FileType go nmap <Leader>dt <Plug>(go-def-tab)
38 au FileType go nmap <Leader>e <Plug>(go-rename)
39 
40 "vim-powerline setting
41 let g:Powerline_symbols='fancy'
42 set laststatus=2
43 
44 " vim-go settings
45 let g:go_fmt_command = "goimports"
46 
47 " YCM settings
48 let g:ycm_key_list_select_completion = ['', '']
49 let g:ycm_key_list_previous_completion = ['', '']
50 let g:ycm_key_invoke_completion = '<C-Space>'
51                                                                   
52 " UltiSnips settings
53 let g:UltiSnipsExpandTrigger="<tab>"
54 let g:UltiSnipsJumpForwardTrigger="<c-b>"
55 let g:UltiSnipsJumpBackwardTrigger="<c-z>"
56 
57 set ts=4 sts=4 sw=4

主要的配置是从8 " set the runtime path to include Vundle and initialize这个地方开始的,Vundle安装插件只需要在vimrc这个文件里面写上Plugin XXX就可以了,在看了Vundle官网之后发现这个是对于插件代码托管在github上面使用的,非托管在这上面的插件就要写上地址的路径了,详细内容可以查看官网。在安装vim-go的时候会检测你GOBIN中是否有gocodegoimports等等这些工具,没有的话它就会自动下载安装,但是由于众所周知的原因,很多工具的安装会失败,所以对于那些工具我们要进行手动安装,可以自己翻墙去下载也可以到gopm这里下载,下载好来后直接使用go build生成可执行文件,然后copy到你的GOBIN中。上面配置文件中的Plugin 'Lokaltog/vim-powerline'跟这里的配置没关系,安装它之后当用vim打开一个文件的使用在最下面会有一个显示栏,显示一些文件的信息。

遇到的问题

整个安装过程就是这样子,期间也遇到来几个问题导致一直安装不成功,第一个在刚才上面提到的博客地址里也有说到,就是在安装好YCM的时候会有这么一段提示

ycm_client_support.[so|pyd|dll] and ycm_core.[so|pyd|dll] not detected; you need to compile YCM before using it. Read the docs!

解决方法原博客里也有说

sudo apt-get install build-essential cmake python-dev
cd ~/.vim/bundle/YouCompleteMe
./install.sh

第二问题就是我在安装插件的时候一直没有出现文中所说的

Vundle.vim会在左侧打开一个Vundle Installer Preview子窗口,窗口下方会提示:“Processing 'fatih/vim-go'”,待安装完毕后,提示信息变 成“Done!”

这个现象,后来我把其它插件都先给注释了,单单就安装Vundle这个插件,就好了,估计是之前没有安装这个插件,以为只要执行

mkdir ~/.vim/bundle
git clone https://github.com/gmarik/Vundle.vim.git

就好了,其实还是要再vim中执行一次插件安装的,毕竟它也是插件,管理插件的插件,^_^。这个安装是在vim内使用:PluginInstall安装的,这下安装好了,就出现来原文所说的显示,心情也好了很多。然后就很顺利的安装了其它的插件,这里godef我在下载来的code.google.com/p/go.tools包中没找到,所以就先略过来,可能是我没看见。不过问题不大。下面图片就是我安装好之后界面

alt

无关的话

前天吧,我将ubuntu桌面切换到了gnome-classic,有个cairo-dock,但是问题来了,之前用的很开的alt+tab组合键切换窗口失效了,然后就去网上找解决方法,总之找了很多中文的一些论坛或者回答的解决方法,但是答案就只有一个,并且连文字都一模一样,包括在哪里加标点,感到十分的惊人。但是他们说的这一个方法也没有解决这个问题。我也不知道那些回答的人到底自己试过没有,估计就是copy一族。本想着再找不到就去stackoverflow上直接问了,后来在google上搜索到了答案,是一个国外的论坛,答案一针见血,十分开心。可能也是我对这些配置不熟悉的缘故吧。 安装Compizconfig setting manage工具,然后打开选中窗口管理,把shift switcher勾选上就OK了,之前的方法一直都是说把Static Application Switcher勾上,所以一直没效果,其实这两个选项是紧靠在一起的,可我每次勾它身边的人却不认识它。

]]>
关于ubuntu更新时出现的一类ppa的404无法找到错误 2014-11-05T00:00:00+00:00 zhengkang /2014/11/关于ubuntu更新时出现的一类ppa的404无法找到错误

刚才在更新xubuntu的时候出现了一个ppa源index无法下载的错误,既然无法找到那就删掉就是了。

错误样例

下面图片是我出错时的截图

标出红线的框框里就是错误的输出信息,由于在这里,出现了这个错误更新停止了,但是我发现其实在更新的时候不止出现了一个这种类型的错误,但是有些错误却不会导致更新停止,要是对于那些错误的ppa更新源我们想要删除的话,就要找出这些ppa源。我们可以使用|grep "Failed"来找出来。

解决方法

下图就是我在本机上删除错误ppa源的方法

删除之后再运行sudo apt-get update,不会再出现之前删除的ppa源导致的错误。

]]>