Skip to content

Commit 7b28904

Browse files
committed
Merge pull request #130 from philips/add-version-to-join2
add versioning to cluster join
2 parents 52cbc89 + b430a07 commit 7b28904

File tree

7 files changed

+164
-5
lines changed

7 files changed

+164
-5
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Versioning
2+
3+
Goal: We want to be able to upgrade an individual machine in an etcd cluster to a newer version of etcd.
4+
The process will take the form of individual followers upgrading to the latest version until the entire cluster is on the new version.
5+
6+
Immediate need: etcd is moving too fast to version the internal API right now.
7+
But, we need to keep mixed version clusters from being started by a rollowing upgrade process (e.g. the CoreOS developer alpha).
8+
9+
Longer term need: Having a mixed version cluster where all machines are not be running the exact same version of etcd itself but are able to speak one version of the internal protocol.
10+
11+
Solution: The internal protocol needs to be versioned just as the client protocol is.
12+
Initially during the 0.\*.\* series of etcd releases we won't allow mixed versions at all.
13+
14+
## Join Control
15+
16+
We will add a version field to the join command.
17+
But, who decides whether a newly upgraded follower should be able to join a cluster?
18+
19+
### Leader Controlled
20+
21+
If the leader controls the version of followers joining the cluster then it compares its version to the version number presented by the follower in the JoinCommand and rejects the join if the number is less than the leader's version number.
22+
23+
Advantages
24+
25+
- Leader controls all cluster decisions still
26+
27+
Disadvantages
28+
29+
- Follower knows better what versions of the interal protocol it can talk than the leader
30+
31+
32+
### Follower Controlled
33+
34+
A newly upgraded follower should be able to figure out the leaders internal version from a defined internal backwards compatible API endpoint and figure out if it can join the cluster.
35+
If it cannot join the cluster then it simply exits.
36+
37+
Advantages
38+
39+
- The follower is running newer code and knows better if it can talk older protocols
40+
41+
Disadvantages
42+
43+
- This cluster decision isn't made by the leader
44+
45+
## Recommendation
46+
47+
To solve the immediate need and to plan for the future lets do the following:
48+
49+
- Add Version field to JoinCommand
50+
- Have a joining follower read the Version field of the leader and if its own version doesn't match the leader then sleep for some random interval and retry later to see if the leader has upgraded.
51+
52+
# Research
53+
54+
## Zookeeper versioning
55+
56+
Zookeeper very recently added versioning into the protocol and it doesn't seem to have seen any use yet.
57+
https://issues.apache.org/jira/browse/ZOOKEEPER-1633
58+
59+
## doozerd
60+
61+
doozerd stores the version number of the machine in the datastore for other clients to check, no decisions are made off of this number currently.

command.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,15 @@ func (c *WatchCommand) Apply(server *raft.Server) (interface{}, error) {
117117

118118
// JoinCommand
119119
type JoinCommand struct {
120+
RaftVersion string `json:"raftVersion"`
120121
Name string `json:"name"`
121122
RaftURL string `json:"raftURL"`
122123
EtcdURL string `json:"etcdURL"`
123124
}
124125

125126
func newJoinCommand() *JoinCommand {
126127
return &JoinCommand{
128+
RaftVersion: r.version,
127129
Name: r.name,
128130
RaftURL: r.url,
129131
EtcdURL: e.url,
@@ -152,14 +154,14 @@ func (c *JoinCommand) Apply(raftServer *raft.Server) (interface{}, error) {
152154
return []byte("join fail"), etcdErr.NewError(103, "")
153155
}
154156

155-
addNameToURL(c.Name, c.RaftURL, c.EtcdURL)
157+
addNameToURL(c.Name, c.RaftVersion, c.RaftURL, c.EtcdURL)
156158

157159
// add peer in raft
158160
err := raftServer.AddPeer(c.Name, "")
159161

160162
// add machine in etcd storage
161163
key := path.Join("_etcd/machines", c.Name)
162-
value := fmt.Sprintf("raft=%s&etcd=%s", c.RaftURL, c.EtcdURL)
164+
value := fmt.Sprintf("raft=%s&etcd=%s&raftVersion=%s", c.RaftURL, c.EtcdURL, c.RaftVersion)
163165
etcdStore.Set(key, value, time.Unix(0, 0), raftServer.CommitIndex())
164166

165167
return []byte("join success"), err

etcd_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"github.com/coreos/go-etcd/etcd"
77
"math/rand"
88
"net/http"
9+
"net/http/httptest"
10+
"net/url"
911
"os"
1012
"strconv"
1113
"strings"
@@ -54,6 +56,53 @@ func TestSingleNode(t *testing.T) {
5456
}
5557
}
5658

59+
// TestInternalVersionFail will ensure that etcd does not come up if the internal raft
60+
// versions do not match.
61+
func TestInternalVersionFail(t *testing.T) {
62+
checkedVersion := false
63+
testMux := http.NewServeMux()
64+
65+
testMux.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
66+
fmt.Fprintln(w, "This is not a version number")
67+
checkedVersion = true
68+
})
69+
70+
testMux.HandleFunc("/join", func(w http.ResponseWriter, r *http.Request) {
71+
t.Fatal("should not attempt to join!")
72+
})
73+
74+
ts := httptest.NewServer(testMux)
75+
defer ts.Close()
76+
77+
fakeURL, _ := url.Parse(ts.URL)
78+
79+
procAttr := new(os.ProcAttr)
80+
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
81+
args := []string{"etcd", "-n=node1", "-f", "-d=/tmp/node1", "-vv", "-C="+fakeURL.Host}
82+
83+
process, err := os.StartProcess("etcd", args, procAttr)
84+
if err != nil {
85+
t.Fatal("start process failed:" + err.Error())
86+
return
87+
}
88+
defer process.Kill()
89+
90+
time.Sleep(time.Second)
91+
92+
_, err = http.Get("http://127.0.0.1:4001")
93+
94+
if err == nil {
95+
t.Fatal("etcd node should not be up")
96+
return
97+
}
98+
99+
if checkedVersion == false {
100+
t.Fatal("etcd did not check the version")
101+
return
102+
}
103+
}
104+
105+
57106
// This test creates a single node and then set a value to it.
58107
// Then this test kills the node and restart it and tries to get the value again.
59108
func TestSingleNodeRecovery(t *testing.T) {

name_url_map.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
// we map node name to url
99
type nodeInfo struct {
10+
raftVersion string
1011
raftURL string
1112
etcdURL string
1213
}
@@ -39,8 +40,9 @@ func nameToRaftURL(name string) (string, bool) {
3940
}
4041

4142
// addNameToURL add a name that maps to raftURL and etcdURL
42-
func addNameToURL(name string, raftURL string, etcdURL string) {
43+
func addNameToURL(name string, version string, raftURL string, etcdURL string) {
4344
namesMap[name] = &nodeInfo{
45+
raftVersion: raftVersion,
4446
raftURL: raftURL,
4547
etcdURL: etcdURL,
4648
}

raft_handlers.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,10 @@ func NameHttpHandler(w http.ResponseWriter, req *http.Request) {
113113
w.WriteHeader(http.StatusOK)
114114
w.Write([]byte(r.name))
115115
}
116+
117+
// Response to the name request
118+
func RaftVersionHttpHandler(w http.ResponseWriter, req *http.Request) {
119+
debugf("[recv] Get %s/version/ ", r.url)
120+
w.WriteHeader(http.StatusOK)
121+
w.Write([]byte(r.version))
122+
}

raft_server.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"crypto/tls"
66
"encoding/json"
7+
"io/ioutil"
78
"fmt"
89
etcdErr "github.com/coreos/etcd/error"
910
"github.com/coreos/go-raft"
@@ -14,6 +15,7 @@ import (
1415

1516
type raftServer struct {
1617
*raft.Server
18+
version string
1719
name string
1820
url string
1921
tlsConf *TLSConfig
@@ -34,6 +36,7 @@ func newRaftServer(name string, url string, tlsConf *TLSConfig, tlsInfo *TLSInfo
3436

3537
return &raftServer{
3638
Server: server,
39+
version: raftVersion,
3740
name: name,
3841
url: url,
3942
tlsConf: tlsConf,
@@ -144,6 +147,7 @@ func (r *raftServer) startTransport(scheme string, tlsConf tls.Config) {
144147

145148
// internal commands
146149
raftMux.HandleFunc("/name", NameHttpHandler)
150+
raftMux.HandleFunc("/version", RaftVersionHttpHandler)
147151
raftMux.Handle("/join", errorHandler(JoinHttpHandler))
148152
raftMux.HandleFunc("/vote", VoteHttpHandler)
149153
raftMux.HandleFunc("/log", GetLogHttpHandler)
@@ -160,15 +164,44 @@ func (r *raftServer) startTransport(scheme string, tlsConf tls.Config) {
160164

161165
}
162166

167+
// getVersion fetches the raft version of a peer. This works for now but we
168+
// will need to do something more sophisticated later when we allow mixed
169+
// version clusters.
170+
func getVersion(t transporter, versionURL url.URL) (string, error) {
171+
resp, err := t.Get(versionURL.String())
172+
173+
if err != nil {
174+
return "", err
175+
}
176+
177+
defer resp.Body.Close()
178+
body, err := ioutil.ReadAll(resp.Body)
179+
180+
return string(body), nil
181+
}
182+
163183
// Send join requests to the leader.
164184
func joinCluster(s *raft.Server, raftURL string, scheme string) error {
165185
var b bytes.Buffer
166186

167-
json.NewEncoder(&b).Encode(newJoinCommand())
168-
169187
// t must be ok
170188
t, _ := r.Transporter().(transporter)
171189

190+
// Our version must match the leaders version
191+
versionURL := url.URL{Host: raftURL, Scheme: scheme, Path: "/version"}
192+
version, err := getVersion(t, versionURL)
193+
if err != nil {
194+
return fmt.Errorf("Unable to join: %v", err)
195+
}
196+
197+
// TODO: versioning of the internal protocol. See:
198+
// Documentation/internatl-protocol-versioning.md
199+
if version != r.version {
200+
return fmt.Errorf("Unable to join: internal version mismatch, entire cluster must be running identical versions of etcd")
201+
}
202+
203+
json.NewEncoder(&b).Encode(newJoinCommand())
204+
172205
joinURL := url.URL{Host: raftURL, Scheme: scheme, Path: "/join"}
173206

174207
debugf("Send Join Request to %s", raftURL)

version.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
package main
22

33
const version = "v1"
4+
5+
// TODO: The release version (generated from the git tag) will be the raft
6+
// protocol version for now. When things settle down we will fix it like the
7+
// client API above.
8+
const raftVersion = releaseVersion

0 commit comments

Comments
 (0)