Giter Site home page Giter Site logo

mit6.824-2021's Introduction

🎓 Received bachelor's and master's degrees from School of Software, Tsinghua University

💻 Interested in consensus algorithm, distributed storage system, time-series database and distributed transaction.

🖋 Blog:tanxinyu.work

💡 Zhihu:tan-xin-yu

📫 Email: [email protected]

💬 Wechat: click here

👷 Check out what I'm currently working on

🌱 Check out my recent projects

⭐ Check out my recent stars

  • elastic/otel-profiling-agent - The production-scale datacenter profiler (C/C++, Go, Rust, Python, Java, NodeJS, PHP, Ruby, Perl, ...) (6 days ago)
  • crate/crate - CrateDB is a distributed and scalable SQL database for storing and analyzing massive amounts of data in near real-time, even with complex queries. It is PostgreSQL-compatible, and based on Lucene. (3 weeks ago)
  • apache/sedona - A cluster computing framework for processing large-scale geospatial data (3 weeks ago)
  • tisonkun/failpoints - An implementation of failpoints for Java. (1 month ago)
  • caicancai/paper_reading_cn - just for fun (2 months ago)

👯 Check out my recent followers

🔨 Check out my recent pull requests

📜 Check out my recent blog posts

mit6.824-2021's People

Contributors

onesizefitsquorum avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

mit6.824-2021's Issues

关于Lab2中末尾提到的liveness的问题

Lab2的末尾提到,用leader上任后提交一条空日志的方式解决所描述的corner case,但是空日志同样没法保证被复制到 majority followers,这样commitIndex还是1,客户端还是能读到旧的数据,貌似这种方法并不能解决这个问题,不知道我理解的对不对,还望赐教。

lab2D快照疑问

根据6.824-2022版的实验要求,链接:https://pdos.csail.mit.edu/6.824/labs/lab-raft.html,有这么一个描述
Previously, this lab recommended that you implement a function called CondInstallSnapshot to avoid the requirement that snapshots and log entries sent on applyCh are coordinated. This vestigal API interface remains, but you are discouraged from implementing it: instead, we suggest that you simply have it return true.

所以我就没有去实现CondInstallSnapshot 接口,把处理快照的所有逻辑(日志清除、往applyChan发送snapshot...)都放在了InstallSnapshot接口,但是会出现bug:server 1 apply out of order, expected index 130, got 150。
如果把处理快照的逻辑全部放在InstallSnapshot接口,请问您觉得有什么需要特别注意的吗?

代码请求

你好,发现你写的代码非常简洁明了, 能否发一份源码学习一下,改进一下我的实现思路
这是我的邮箱[email protected],谢谢~

请教有关lab 2A的问题

看了大佬的代码有点疑问:

func (rf *Raft) BroadcastHeartbeat(isHeartBeat bool) {
	for peer := range rf.peers {
		if peer == rf.me {// ❓
			continue
		}
		if isHeartBeat {
			// need sending at once to maintain leadership
			go rf.replicateOneRound(peer)
		} else {
			// just signal replicator goroutine to send entries in batch
			rf.replicatorCond[peer].Signal()
		}
	}
}

该方法是作为leader才会调用,“❓”处我觉得会使得这个leader自己election timeout,确切地说作为leader没有什么办法可以reset这个ticker中的election timeout。
然后会有这样的输出:warning: term changed even though there were no failures
是否是我理解有误?多谢指教,感激不尽😄😄😄

关于follower拒绝AppendEntries RPC的表现

if !rf.matchLog(args.PrevLogTerm, args.PrevLogIndex) {
    reply.Term, reply.Success = rf.currentTerm, false
    lastIndex := rf.getLastLog().Index
    if lastIndex < args.PrevLogIndex {
        reply.ConflictTerm, reply.ConflictIndex = -1, lastIndex+1
    } else {
        firstIndex := rf.getFirstLog().Index
        reply.ConflictTerm = rf.logs[args.PrevLogIndex-firstIndex].Term
        index := args.PrevLogIndex - 1
        for index >= firstIndex && rf.logs[index-firstIndex].Term == reply.ConflictTerm {
            index--
        }
        reply.ConflictIndex = index
    }
    return
}

这一段中,如果follower与leader的日志不匹配,可以将conflictTerm和conflictIndex给到leader,让leader快速回退到冲突位置,而不需要递减index。这里的两种情况:

  1. follower日志过短,跟不上follower日志

    在这里conflictTerm设置为-1,conflictIndex设置为follower最后一条日志的index+1

  2. 正常的冲突

    这里将conflictTerm只设置为要同步日志的任期,按我的理解,如果已经同步的位置到这个位置中间有有多个不同的任期,这里还是需要进行等同不同任期数量次数的请求才能到同步点的后一个位置

    然后这里我不懂的是这里设置conflictIndex为index,在for循环中,退出条件是term != conflictTerm,当这里已经到同步点后的任期时,这里退出循环时index就会等于同步点的index

这里两种情况,第一个conflictIndex就是冲突位置,第二个则是冲突位置的前一个,这里的设计是否有错误?


另外我想参考一下handleAppendEntriesReply的具体实现,因为我感觉conflictTerm好像并没有起到什么实际作用,可以直接将conflictIndex赋值到nextIndex也没什么问题(按照conflictIndex就是冲突位置的逻辑)

Lab2B 收到 RequestVote 请求 和 electionTimer 超时同时发生, 似乎会有并发问题?

image image

图中标注 "1", "2", "3" 的地方.
假设当前节点是 Follower, 协程 1 的 electionTimer 发生了超时但尚未拿到锁, 而另一个协程 2 收到了 RequestVote 请求并拿到锁进行处理. 那么协程 2 会投票给另一个节点 (也重置了 electionTimer), 最后释放锁.

协程 1 拿到锁后, 再发起调用 StartElection() 似乎会有一些问题: 另一个节点收到过半数投票后成为了 Leader 后接收 command 却没能发送给其他结点. 因为当前节点会将 term+1, 再发送给其他结点包括 Leader, 导致 Leader 变为 Follower(因为当前节点的 term 更大).

一个可能的解决方案是: 在协程 1 拿到锁后再 double check 一下当前是否已经超时?

关于lab2的CondInstallSnapshot

在课程给出的raft论文中,是没有CondInstallSnapshot这个接口的,对日志的压缩操作是放在InstallSnapshot这个接口的。请问为什么要把日志压缩的操作放在CondInstallSnapshot这里呢,请问有什么理论根据吗。我发现如果把日志压缩的操作放在InstallSnapshot里,是没有办法通过lab4b的testConcurrent3的,还望解答。

Lab4B push or pull

博主您好!关于分片迁移的push模式我想问一下,以下这种case是如何解决的,是否会发生呢?groupA和groupB在ConfigNum为1时,B需要发送shard数据给A,并且A接收成功,B发送成功了,然后A和B正常运行到达了更高的ConfigNum,之后若A在没有persist数据时宕机重启了,ConfigNum重置为0了,当它再次到达ConfigNum为1时,是否会一直阻塞在此呢,因为B已经不会再给它发送数据了。换句话说,ConfigNum是否需要连续递增?能否直接query到高版本的Config,然后进行数据迁移?

关于 lab4B 中的分片迁移模式的讨论(push/pull)

博主你好,对于lab4B 中的代码实现, GetShardsData 的实现中,博主做了 configNum 检查之后,就直接返回分片数据了;这里是不是需要主动调用一次 raft.Start 来确保 group 是正常的?
这里我提个我自己的理解,不知道是否有误,考虑一个 group 内 5 个 server 出现分区的情况:
导致 [oldLeader, server] [newLeader, server, server],在一个 configNum 周期内, newLeader 和客户端通讯并跟新了自己的数据,然后同时也存在分片迁移,另一个 group 将 pulling 发送给了 oldLeader,oldLeader 直接返回旧的分片数据了,这是否有可能发生?

======================= 分割线 ===================
然后这边想和你讨论一下 push 和 pull 的实现,我这里一开始尝试实现 push,但是发现不太好搞QAQ
对于分片迁移的 push 和 pull 模式,我有思考过一点,但是感觉还是有一点不同
考虑 push 模式和 pull 模式的不同:

如果是 push 模式,存在 G1 和 G2,G1 需要 push 分片数据给 G2,push 成功后需要 GC,即:
G1 ===== push args ======> G2
G2 Start -> apply
G1 <==== push reply ======= G2
一次 rpc 即可知道 push 成功了,随即 G1 即可进行 GC
G1 Start GC -> apply

如果是 pull 模式,G1 需要从G2 pull 分片数据,pull 成功后,G2 需要 GC,即:
G1 ===== pull args ======> G2
G2 Start -> apply(这里我看博主的代码是没有这一步的)
G1 <===== pull reply ====== G2
G1 Start -> apply
G2 并不能明确 pull reply 是否真正到达,只有 G1 才可以明确,所以需要 G1 重试,G2 接收到后方可GC
G1 ===== GC args =====> G2
G2 Start GC -> apply
G1 <===== GC reply ===== G2

初步猜想,各有利弊,push 模式比 pull 模式少一次 rpc 调用
但经过故障考虑,还是使用 pull 模式更佳,考虑这样的一个情况:
若使用 pushing 模式,一个 group 故障后重启,则 configNum 可能会从 低版本 开始,此时它会以落后的 configNum push 数据给其他 group,
其他 group 检测到落后 configNum 的 push,可以直接返回 OK 即可,但是其落后的 group 将永远拿不到需要从其他 group pulling 的数据,因为是 push 模式,需要其他 group 主动推,但是其他 group 已经是最新的 configNum;
所以这种实现 push 模式会实现不了,这个考虑对吗?

请问选举和心跳计时器的时间应该设置为多少?

我在一直无法通过ab3的TestSpeed3A测试,他提示我test_test.go:413: Operations completed too slowly 61.653846ms/op > 33.333333ms/op,完成每次operation的用时太长了。我看了日志,从调用Start()到raft层apply的用时一开始是几毫秒到了后期增加到几百毫秒。我觉得可能有计时器时间设置的问题,可以为我提供一些建议吗?

关于Lab3A

你好,参考了你的设计遇到了这样一个问题:
node0为主结点,接收到{clientId: 1, seqNum: 1, op:Get},并将该命令用Start发到raft层,得到CommandIndex: 1。接下来kv.Command 函数创建了notifyChan1,等待kv.applier发送response。

此时raft的node0挂掉,随后又被选举为主结点。这个过程中,kv.Command超时,导致RPC返回了ErrTimeout。(这个时候notifyChan1没有收到response,是否以后再也无法收到?)

node0恢复为主结点后,raft.applier发送applyMsg到kv.applier,然后kv.applier发送response到notifyChan1,但是由于kv.Command早就超时返回,notifyChan1不会接收response。此时,kv.applier阻塞。

客户端重发RPC,kv.Command,但是因为kv.applier的堵塞,并且它还持有kv.mu.lock,所以kv.Command也阻塞。死锁。

请问我哪里理解错误了?

关于Lab3的性能问题

Lab3中有一个SpeedTest,要求每轮heartbeat至少要执行三条命令,请问你是如何做到的?

即使假设网络通讯可靠无延迟,如果只有一个客户端串行执行命令的话,每轮heartbeat最多只能执行一条命令。

我一开始使用的就是简单的串行客户端,需要100s才能完成SpeedTest,显然是远远不能满足性能要求。当然我尝试过用异步或缓存来提高性能,但是总会造成线性化测试的失败。

SpeedTest测试流程的伪代码如下:

var ck1, ck2 Clerk
v := ck1.Get("0")  // 这个应该是用来发现并保存leader的

begin := time.Now()
for i := 0; i < 1000; i++ {
    ck1.Append("0", " " + i)
}
dur := time.Since(begin)  // 每轮heartbeat相隔100ms,若每轮执行3条指令,1000条指令应在34s内完成

v = ck2.Get("0")  // 线性化要求这条命令应得到 " 0 1 2 3 4 ... 999"

请教有关Raft paper Figure 8的问题

在Figure 8的阶段c中,S1收到了大多数机器(S2、S3)对index=2,term=2的log的确认,在问题出现之前apply到了上层应用,并且返回给客户。S1在发送包含新的commitIndex的AE之前,发生网络分区,导致S1可以与客户通信,但是无法与peer通信。接着S5重启,到了d阶段,覆盖了S2、S3、S4的index=2处的log。此时有新的主机与新Leader S5通信,使得index=2,term=3的log被提交给上层应用。S1自始至终都没有crash,那么与S1通信的客户收到的对index=2,term=2的操作的响应是不是违反了raft的线性一致性呢?这里一直没有想明白,希望大佬解答。

请问Lab2B的tester是怎么检查committed的log的

测试Test 2的TestBasicAgree2B遇到的问题,
我打印三个peers的log列表发现已经是同步的
截屏2023-05-18 18 09 00
但是tester总是不认为我已经提交了log,
查看config.logs 描述的是
logs []map[int]interface{} // copy of each server's committed entries
但是也看遍了代码也看不出来是怎么复制我的raft中的log的。
请问这个测试器是怎么检查raft里的logs的呢

lab2 定时器问题

通过rf.electionTimer.Reset(RandomizedElectionTimeout())重置定时器以及select内上锁是不对的,会存在一些race情况,加大测试次数会发现这个问题。
我认为的正确方法:(也不一定正确)

  1. select前后上锁(也即对定时器管道触发上锁)。
  2. 定时器reset参考Go Timer 详解以及 Reset 和 Stop 的正确用法进行管道清理和合理重置。

lab3A

“仅对当前 term 日志的 notifyChan 进行通知:上一点提到,对于 leader 降级为 follower 的情况,该节点需要让阻塞的请求超时重试以避免违反线性一致性。那么有没有这种可能呢?leader 降级为 follower 后又迅速重新当选了 leader,而此时依然有客户端协程未超时在阻塞等待,那么此时 apply 日志后,根据 index 获得 channel 并向其中 push 执行结果就可能出错,因为可能并不对应。”

您好,我不是很理解为什么要去判断currentTerm,您这里说的“因为可能并不对应”到底是什么情况呢?可否详细展开讲。对应的代码是

// only notify related channel for currentTerm's log when node is leader
				if currentTerm, isLeader := kv.rf.GetState(); isLeader && message.CommandTerm == currentTerm {
					ch := kv.getNotifyChan(message.CommandIndex)
					ch <- response
				}

关于lab2Dsnapshot函数的讨论

您好,我在snapshot这一个函数里,碰到了一个错误,不知道您碰到过没,就是snapshot函数里rf.log[index – rf.getFirstEntry().Index:],偶尔会报错,例如slice[10:8],一直没查出来原因,而且只是偶尔会有这个错误。

Start()后广播的时机

请问您的实现中,当raft被调用Start()后,将这条entry同步的时机时什么时候呢?是立即广播还是等到下一次心跳呢?
我目前遇到的问题是在Lab 3A的TestSpeed3A,会超时,我改成了每次都立即广播,虽然效果很好但发送的rpc太多。
我觉得应该是和我设置的心跳超时时间有关系,如果设置得很短比如15ms,就不会出现这个问题

关于lab2D的疑问

您好,想请教一个关于applier和InstallSnapshot(下文简称is)的同步问题。

首先看了测试代码(config.go/line 212, applierSnap函数),我的理解是,applier里发送给channel的命令会直接apply,检查index;而is会先调condis判断是否有lastIncludedIndex > rf.commitIndex,再决定是否apply这个snapshot。

设想如下情况:
某个follower此时lastApplied = 13, commitIndex = 18。 applier中切好了log entries,然后释放锁,开始遍历发送给channel。
比如发完15之后,leader发来lastIncludedIndex=19的snapshot。is,condis检查19>18,于是apply了这个snapshot。之后lastApplied = 19, commitIndex = 19。那is结束之后,applier里继续apply entry 16,由于不是snapshot,所以会直接应用,并检查下标是否是lastapplied + 1。那不是会error吗?

想问一下您的代码是如何避免这个情形的,没太想明白我对您的代码哪里理解有误。感谢!

关于lab3中kvservice层持久化状态的一些疑问

我的实现是在kv层的applier中每从applych中读出一个applymsg,判断下raftstatesize是否达到阈值,若达到则为kv层打快照并下发给raft层。除此之外是否还需要在什么时候持久化kv层的状态,就类似raft层中持久化term、log等状态一样。因为我遇到了一个问题 test_test.go:144: duplicate element x 0 31 y in Append result。 我查看了日志发现是每个kvserver都已将同一条append RPC应用于状态机并更新了session map,这里没有打快照,然后在回复clerk之前被测试程序kill了。

关于lab2 AppendEntries success 时的边界问题

firstIndex := rf.getFirstLog().Index
	for index, entry := range request.Entries {
		if entry.Index-firstIndex >= len(rf.logs) || rf.logs[entry.Index-firstIndex].Term != entry.Term {
			rf.logs = shrinkEntriesArray(append(rf.logs[:entry.Index-firstIndex], request.Entries[index:]...))
			break
		}
	}

这里 entry.Index-firstIndex >= len(rf.logs) 的大于会在什么情况下会满足呢?是 follower 已经把部分entries included 进 snapshot 所以可能大于是吗?

关于lab3 重启kv系统时重放raft日志问题

假设有以下场景:
用户A发送PUT(X, 2)的请求R1,交给了下层的raft,返回的index=i。
用户B发送PUT(X, 3)的请求R2,交给了下层的raft,返回的index=i+1。
用户A设置的的请求超时时间较短,没有等Raft完成日志复制,重新发送了ClientId和commandId与R1相同的R3,交接了下层的raft,返回的index=i+2。

虽然在apply到状态机时可以做到apply only once,但是重放raft日志时,应该为3的X的值变成了2。

问题:
如何避免上述场景呢?还是我的理解有问题,望解答~

关于日志复制的一点问题

在论文中有提到,对于一个leader,If AppendEntries fails because of log inconsistency: decrement nextIndex and retry (§5.3)。
在作者的笔记和代码中,handleAppendEntriesResponse是领导者处理日志复制响应的方法,这个方法在遇到日志不一致的情况时,作者你对rf.nextIndex[peer]做了递减的操作,但是没有直接在这个方法里retry。
所以是由之后的ticker触发heartbeat时再重新尝试AppendEntries吗? 这样处理和论文中提到的在AppendEntries fails之后直接 retry 有什么不同和影响吗?
感谢作者!希望能得到解答!

LAB2实现中可能存在的活锁

func (rf *Raft) RequestVote(request *RequestVoteRequest, response *RequestVoteResponse) {
	rf.mu.Lock()
	defer rf.mu.Unlock()
	defer rf.persist()
	defer DPrintf("{Node %v}'s state is {state %v,term %v,commitIndex %v,lastApplied %v,firstLog %v,lastLog %v} before processing requestVoteRequest %v and reply requestVoteResponse %v", rf.me, rf.state, rf.currentTerm, rf.commitIndex, rf.lastApplied, rf.getFirstLog(), rf.getLastLog(), request, response)

	if request.Term < rf.currentTerm || (request.Term == rf.currentTerm && rf.votedFor != -1 && rf.votedFor != request.CandidateId) {
		response.Term, response.VoteGranted = rf.currentTerm, false
		return
	}
	if request.Term > rf.currentTerm {
		rf.ChangeState(StateFollower)
		rf.currentTerm, rf.votedFor = request.Term, -1
	}
	if !rf.isLogUpToDate(request.LastLogTerm, request.LastLogIndex) {
		response.Term, response.VoteGranted = rf.currentTerm, false
		return
	}
	rf.votedFor = request.CandidateId
	rf.electionTimer.Reset(RandomizedElectionTimeout())
	response.Term, response.VoteGranted = rf.currentTerm, true
}

在选主的handler中,有可能出现:当前节点的Term比Candidate小,且原先是Leader,且自身的日志是upToDate的,但candidate的日志不是up-to-date。这样的话,该节点就会经过这一段代码:

	if request.Term > rf.currentTerm {
		rf.ChangeState(StateFollower)
		rf.currentTerm, rf.votedFor = request.Term, -1
	}
	if !rf.isLogUpToDate(request.LastLogTerm, request.LastLogIndex) {
		response.Term, response.VoteGranted = rf.currentTerm, false
		return
	}

从而使得该节点的electionTimer没有被reset,从而不能成为Candidate。

有可能网络中出现这样的情况:
五个节点,出现了三个网络划分:L | L | L F F

  1. 此时第3个划分中heartbeat传输很慢,但在等待返回的时候第3个Leader还在不断收到entry
  2. 不久后第3个划分中某个Follower超时,成为Candidate
  3. 与此同时,网络不再划分,5个节点都能收到该Candidate的requestVoteRPC
  4. 此时该Candidate的term是所有节点中最大的,但与前三个原Leader节点相比,该Candidate的日志不是up-to-date的,因为:
    • Candidate日志的最后一项是在term很小的时候加入的,而前两个原Leader节点日志的最后一项有更大的term
    • Candidate日志最后一项和第三个原Leader节点日志的最后一项相同,但由于2.,原Leader节点的日志更长
  5. 这导致前三个原Leader节点走上面的路径,没有reset自己的electionTimer,从而让选举只有后两个能成为Candidate,从而让所有能成为Leader的节点无法成为Candidate,从而无法选出Leader

不知道是否遇到这样的情况?若有的话请问是如何解决的呢?

有关liveness的问题2可能并不存在

因为heartbeat也会进行日志的同步,所以我觉得您问题2中描述的liveness的问题并不存在。
在您举得例子中,节点1失效,节点2当选leader(logs 是 [1,2],commitIndex 和 lastApplied 是 1),节点3为落后的follower(logs 是 [1],commitIndex 和 lastApplied 是 1)。此时,leader的nextIndex[3]默认为乐观估计的3,即 leader last log index + 1。在发送heartbeat时,prevLogIndex为2,节点3会及时发现日志不一致,并尽快同步到 [1,2]的状态,随之commitIndex , lastApplied均会得到更新,而不必等到下一次写请求过来才完成日志的同步工作。

即使代码正确,Lab2B的前几个 Task 是不是也有极小概率 Fail ?

我注意到,比如:

  • TestBasicAgree2B
  • TestFollowerFailure2B
  • TestLeaderFailure2B
    这三个任务中的 cfg.one() 方法的最后一个参数是 retry=false,这就意味着,只会调用 rf.Start() 一次,有可能出现接收该请求的 Leader 还未提交日志就成为了 Follower,然后由于 retry=false,所以不会有新的请求进来(不会有新的 rf.Start() 的调用),这个 log 就一直提交不了(旧任期的 Log 不能被新任期的 Leader 提交),然后 2 秒后超时,导致测试失败,并提示"one(%v) failed to reach agreement"

因为 Lab2 是没有让实现 "no-op" 机制的,所以只要遇见这种情况,测试就会失败?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.