diff --git a/tools/benchmark/cmd/put.go b/tools/benchmark/cmd/put.go
new file mode 100644
index 000000000..d71db367e
--- /dev/null
+++ b/tools/benchmark/cmd/put.go
@@ -0,0 +1,107 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+	"time"
+
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/cheggaaa/pb"
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/cobra"
+	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
+	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
+	"github.com/coreos/etcd/etcdserver/etcdserverpb"
+)
+
+// putCmd represents the put command
+var putCmd = &cobra.Command{
+	Use:   "put",
+	Short: "Benchmark put",
+
+	Run: putFunc,
+}
+
+var (
+	keySize int
+	valSize int
+
+	putTotal int
+)
+
+func init() {
+	RootCmd.AddCommand(putCmd)
+	putCmd.Flags().IntVar(&keySize, "key-size", 8, "Key size of put request")
+	putCmd.Flags().IntVar(&valSize, "val-size", 8, "Value size of put request")
+	putCmd.Flags().IntVar(&putTotal, "total", 10000, "Total number of put requests")
+}
+
+func putFunc(cmd *cobra.Command, args []string) {
+	results = make(chan *result, putTotal)
+	requests := make(chan *etcdserverpb.PutRequest, putTotal)
+	bar = pb.New(putTotal)
+
+	k, v := mustRandBytes(keySize), mustRandBytes(valSize)
+
+	conns := make([]*grpc.ClientConn, totalConns)
+	for i := range conns {
+		conns[i] = mustCreateConn()
+	}
+
+	clients := make([]etcdserverpb.KVClient, totalClients)
+	for i := range clients {
+		clients[i] = etcdserverpb.NewKVClient(conns[i%int(totalConns)])
+	}
+
+	bar.Format("Bom !")
+	bar.Start()
+
+	for i := range clients {
+		wg.Add(1)
+		go doPut(clients[i], requests)
+	}
+
+	start := time.Now()
+	for i := 0; i < putTotal; i++ {
+		r := &etcdserverpb.PutRequest{
+			Key:   k,
+			Value: v,
+		}
+		requests <- r
+	}
+	close(requests)
+
+	wg.Wait()
+
+	bar.Finish()
+	printReport(putTotal, results, time.Now().Sub(start))
+}
+
+func doPut(client etcdserverpb.KVClient, requests <-chan *etcdserverpb.PutRequest) {
+	defer wg.Done()
+
+	for r := range requests {
+		st := time.Now()
+		_, err := client.Put(context.Background(), r)
+
+		var errStr string
+		if err != nil {
+			errStr = err.Error()
+		}
+		results <- &result{
+			errStr:   errStr,
+			duration: time.Now().Sub(st),
+		}
+		bar.Increment()
+	}
+}
diff --git a/tools/benchmark/cmd/range.go b/tools/benchmark/cmd/range.go
new file mode 100644
index 000000000..b4799205b
--- /dev/null
+++ b/tools/benchmark/cmd/range.go
@@ -0,0 +1,113 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"time"
+
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/cheggaaa/pb"
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/cobra"
+	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
+	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
+	"github.com/coreos/etcd/etcdserver/etcdserverpb"
+)
+
+// rangeCmd represents the range command
+var rangeCmd = &cobra.Command{
+	Use:   "range key [end-range]",
+	Short: "Benchmark range",
+
+	Run: rangeFunc,
+}
+
+var (
+	rangeTotal int
+)
+
+func init() {
+	RootCmd.AddCommand(rangeCmd)
+	rangeCmd.Flags().IntVar(&rangeTotal, "total", 10000, "Total number of range requests")
+}
+
+func rangeFunc(cmd *cobra.Command, args []string) {
+	if len(args) == 0 || len(args) > 2 {
+		fmt.Fprintln(os.Stderr, cmd.Usage())
+		os.Exit(1)
+	}
+
+	k := []byte(args[0])
+	var end []byte
+	if len(args) == 1 {
+		end = []byte(args[1])
+	}
+
+	results = make(chan *result, rangeTotal)
+	requests := make(chan *etcdserverpb.RangeRequest, rangeTotal)
+	bar = pb.New(rangeTotal)
+
+	conns := make([]*grpc.ClientConn, totalConns)
+	for i := range conns {
+		conns[i] = mustCreateConn()
+	}
+
+	clients := make([]etcdserverpb.KVClient, totalClients)
+	for i := range clients {
+		clients[i] = etcdserverpb.NewKVClient(conns[i%int(totalConns)])
+	}
+
+	bar.Format("Bom !")
+	bar.Start()
+
+	for i := range clients {
+		wg.Add(1)
+		go doRange(clients[i], requests)
+	}
+
+	start := time.Now()
+	for i := 0; i < rangeTotal; i++ {
+		r := &etcdserverpb.RangeRequest{
+			Key:      k,
+			RangeEnd: end,
+		}
+		requests <- r
+	}
+	close(requests)
+
+	wg.Wait()
+
+	bar.Finish()
+	printReport(rangeTotal, results, time.Now().Sub(start))
+}
+
+func doRange(client etcdserverpb.KVClient, requests <-chan *etcdserverpb.RangeRequest) {
+	defer wg.Done()
+
+	for req := range requests {
+		st := time.Now()
+		_, err := client.Range(context.Background(), req)
+
+		var errStr string
+		if err != nil {
+			errStr = err.Error()
+		}
+		results <- &result{
+			errStr:   errStr,
+			duration: time.Now().Sub(st),
+		}
+		bar.Increment()
+	}
+}
diff --git a/tools/v3benchmark/report.go b/tools/benchmark/cmd/report.go
similarity index 99%
rename from tools/v3benchmark/report.go
rename to tools/benchmark/cmd/report.go
index 785ea440a..04459bb31 100644
--- a/tools/v3benchmark/report.go
+++ b/tools/benchmark/cmd/report.go
@@ -14,7 +14,7 @@
 
 // the file is borrowed from github.com/rakyll/boom/boomer/print.go
 
-package main
+package cmd
 
 import (
 	"fmt"
diff --git a/tools/benchmark/cmd/root.go b/tools/benchmark/cmd/root.go
new file mode 100644
index 000000000..124da187a
--- /dev/null
+++ b/tools/benchmark/cmd/root.go
@@ -0,0 +1,57 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"sync"
+
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/cheggaaa/pb"
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/cobra"
+)
+
+// This represents the base command when called without any subcommands
+var RootCmd = &cobra.Command{
+	Use:   "benchmark",
+	Short: "A low-level benchmark tool for etcd3",
+	Long: `benchmark is a low-level benchmakr tool for etcd3.
+It uses gRPC client directly and does not depend on 
+etcd client libray.
+	`,
+}
+
+var (
+	endpoints    string
+	totalConns   uint
+	totalClients uint
+
+	bar     *pb.ProgressBar
+	results chan *result
+	wg      sync.WaitGroup
+)
+
+func init() {
+	RootCmd.PersistentFlags().StringVar(&endpoints, "endpoint", "127.0.0.1:2378", "comma-separated gRPC endpoints")
+	RootCmd.PersistentFlags().UintVar(&totalConns, "conns", 1, "Total number of gRPC connections")
+	RootCmd.PersistentFlags().UintVar(&totalClients, "clients", 1, "Total number of gRPC clients")
+}
+
+func Execute() {
+	if err := RootCmd.Execute(); err != nil {
+		fmt.Println(err)
+		os.Exit(-1)
+	}
+}
diff --git a/tools/benchmark/cmd/util.go b/tools/benchmark/cmd/util.go
new file mode 100644
index 000000000..74394f2d6
--- /dev/null
+++ b/tools/benchmark/cmd/util.go
@@ -0,0 +1,42 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+	"crypto/rand"
+	"fmt"
+	"os"
+
+	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
+)
+
+func mustCreateConn() *grpc.ClientConn {
+	conn, err := grpc.Dial(endpoints)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "dial error: %v\n", err)
+		os.Exit(1)
+	}
+	return conn
+}
+
+func mustRandBytes(n int) []byte {
+	rb := make([]byte, n)
+	_, err := rand.Read(rb)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "failed to generate value: %v\n", err)
+		os.Exit(1)
+	}
+	return rb
+}
diff --git a/tools/benchmark/main.go b/tools/benchmark/main.go
new file mode 100644
index 000000000..b6cb5548f
--- /dev/null
+++ b/tools/benchmark/main.go
@@ -0,0 +1,29 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/coreos/etcd/tools/benchmark/cmd"
+)
+
+func main() {
+	if err := cmd.RootCmd.Execute(); err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		os.Exit(-1)
+	}
+}
diff --git a/tools/v3benchmark/get.go b/tools/v3benchmark/get.go
deleted file mode 100644
index a411e9a17..000000000
--- a/tools/v3benchmark/get.go
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright 2015 CoreOS, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package main
-
-import (
-	"time"
-
-	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
-	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
-	"github.com/coreos/etcd/etcdserver/etcdserverpb"
-)
-
-func benchGet(conn *grpc.ClientConn, key, rangeEnd []byte, n, c int) {
-	wg.Add(c)
-	requests := make(chan struct{}, n)
-
-	for i := 0; i < c; i++ {
-		go get(etcdserverpb.NewKVClient(conn), key, rangeEnd, requests)
-	}
-
-	for i := 0; i < n; i++ {
-		requests <- struct{}{}
-	}
-	close(requests)
-}
-
-func get(client etcdserverpb.KVClient, key, end []byte, requests <-chan struct{}) {
-	defer wg.Done()
-	req := &etcdserverpb.RangeRequest{Key: key, RangeEnd: end}
-
-	for _ = range requests {
-		st := time.Now()
-		_, err := client.Range(context.Background(), req)
-
-		var errStr string
-		if err != nil {
-			errStr = err.Error()
-		}
-		results <- &result{
-			errStr:   errStr,
-			duration: time.Now().Sub(st),
-		}
-		bar.Increment()
-	}
-}
diff --git a/tools/v3benchmark/main.go b/tools/v3benchmark/main.go
deleted file mode 100644
index 22dd3fe1a..000000000
--- a/tools/v3benchmark/main.go
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright 2015 CoreOS, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"os"
-	"strconv"
-	"sync"
-	"time"
-
-	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/cheggaaa/pb"
-	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
-)
-
-var (
-	bar     *pb.ProgressBar
-	results chan *result
-	wg      sync.WaitGroup
-)
-
-func main() {
-	var (
-		c, n int
-		url  string
-		size int
-	)
-
-	flag.IntVar(&c, "c", 50, "number of connections")
-	flag.IntVar(&n, "n", 200, "number of requests")
-	flag.IntVar(&size, "s", 128, "size of put request")
-	// TODO: config the number of concurrency in each connection
-	flag.StringVar(&url, "u", "127.0.0.1:12379", "etcd server endpoint")
-	flag.Parse()
-	if flag.NArg() < 1 {
-		flag.Usage()
-		os.Exit(1)
-	}
-
-	var act string
-	if act = flag.Args()[0]; act != "get" && act != "put" {
-		fmt.Printf("unsupported action %v\n", act)
-		os.Exit(1)
-	}
-
-	conn, err := grpc.Dial(url)
-	if err != nil {
-		fmt.Errorf("dial error: %v", err)
-		os.Exit(1)
-	}
-
-	results = make(chan *result, n)
-	bar = pb.New(n)
-	bar.Format("Bom !")
-	bar.Start()
-
-	start := time.Now()
-
-	if act == "get" {
-		var rangeEnd []byte
-		key := []byte(flag.Args()[1])
-		if len(flag.Args()) > 2 {
-			rangeEnd = []byte(flag.Args()[2])
-		}
-		benchGet(conn, key, rangeEnd, n, c)
-	} else if act == "put" {
-		key := []byte(flag.Args()[1])
-		// number of different keys to put into etcd
-		kc, err := strconv.ParseInt(flag.Args()[2], 10, 32)
-		if err != nil {
-			panic(err)
-		}
-		benchPut(conn, key, int(kc), n, c, size)
-	}
-
-	wg.Wait()
-
-	bar.Finish()
-	printReport(n, results, time.Now().Sub(start))
-}
diff --git a/tools/v3benchmark/put.go b/tools/v3benchmark/put.go
deleted file mode 100644
index fe70918cc..000000000
--- a/tools/v3benchmark/put.go
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright 2015 CoreOS, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package main
-
-import (
-	"crypto/rand"
-	"encoding/binary"
-	"fmt"
-	"os"
-	"time"
-
-	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
-	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
-	"github.com/coreos/etcd/etcdserver/etcdserverpb"
-)
-
-func benchPut(conn *grpc.ClientConn, key []byte, kc, n, c, size int) {
-	wg.Add(c)
-	requests := make(chan *etcdserverpb.PutRequest, n)
-
-	v := make([]byte, size)
-	_, err := rand.Read(v)
-	if err != nil {
-		fmt.Printf("failed to generate value: %v\n", err)
-		os.Exit(1)
-		return
-	}
-
-	for i := 0; i < c; i++ {
-		go put(etcdserverpb.NewKVClient(conn), requests)
-	}
-
-	suffixb := make([]byte, 8)
-	suffix := 0
-	for i := 0; i < n; i++ {
-		binary.BigEndian.PutUint64(suffixb, uint64(suffix))
-		r := &etcdserverpb.PutRequest{
-			Key:   append(key, suffixb...),
-			Value: v,
-		}
-		requests <- r
-		if suffix > kc {
-			suffix = 0
-		}
-		suffix++
-	}
-	close(requests)
-}
-
-func put(client etcdserverpb.KVClient, requests <-chan *etcdserverpb.PutRequest) {
-	defer wg.Done()
-
-	for r := range requests {
-		st := time.Now()
-		_, err := client.Put(context.Background(), r)
-
-		var errStr string
-		if err != nil {
-			errStr = err.Error()
-		}
-		results <- &result{
-			errStr:   errStr,
-			duration: time.Now().Sub(st),
-		}
-		bar.Increment()
-	}
-}